{"id":39593,"date":"2025-12-17T07:00:00","date_gmt":"2025-12-17T15:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=39593"},"modified":"2025-12-10T16:20:37","modified_gmt":"2025-12-11T00:20:37","slug":"dynamic-ad-supported-video-playlist-next-js","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js","title":{"rendered":"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Most video players handle one clip and stop there. They don\u2019t handle playlists, mid-roll ads, or smooth handoffs between videos. This project fixes that.<\/p>\n<p>You\u2019ll build a small, flexible system that plays a set of videos, inserts ads, and switches sources without disrupting the viewer experience. Cloudinary delivers every asset, and Next.js handles the playback logic on the client.<\/p>\n<p>The result is a simple, fast player that feels closer to a real streaming app.<\/p>\n<h2>What We\u2019re Building<\/h2>\n<ul>\n<li>A playlist that moves from one video to the next.<\/li>\n<li>Mid-roll ads that trigger at any second you choose.<\/li>\n<li>Tools to skip ads, test ads, and change settings on the fly.<\/li>\n<li>Clean components you can extend or replace.<\/li>\n<\/ul>\n<p>This setup is easy to adapt for learning platforms, tutorials, creator content, or lightweight streaming features.<\/p>\n<ul>\n<li>\n<strong>Live demo:<\/strong> <a href=\"https:\/\/ad-player-rho.vercel.app\/\">https:\/\/ad-player-rho.vercel.app\/<\/a>\n<\/li>\n<li>\n<strong>GitHub repo:<\/strong> <a href=\"https:\/\/github.com\/musebe\/ad-player\">https:\/\/github.com\/musebe\/ad-player<\/a>\n<\/li>\n<\/ul>\n<h2>Project Overview<\/h2>\n<p>This project uses three main parts that work together.<\/p>\n<ol>\n<li>\n<p><strong>Next.js for the player logic.<\/strong> Next.js gives you a fast client experience. You control playback with React state, effects, and small components. Each part of the player stays simple and easy to change.<\/p>\n<\/li>\n<li>\n<p><strong>Cloudinary for video delivery.<\/strong> Cloudinary stores and serves every video. You only need the public IDs. The player pulls videos from folders like:<\/p>\n<\/li>\n<\/ol>\n<ul>\n<li>\n<code>videos\/main\/<\/code> for your main content.<\/li>\n<li>\n<code>videos\/ads\/<\/code> for ad clips.<\/li>\n<\/ul>\n<p>Cloudinary handles streaming, formats, and device support.<\/p>\n<ol start=\"3\">\n<li>\n<strong>A playlist system with mid roll ads.<\/strong> The player loads a list of videos. It plays them one by one. During playback, it checks the current time and switches to an ad clip when needed. After the ad ends, the player returns to the exact second it left off.<\/li>\n<\/ol>\n<p>You can change the ad video, when it plays, the ad link, and the current item in the playlist. This gives you a small, flexible base that behaves like a simple streaming workflow.<\/p>\n<h2>Start With Next.js and Cloudinary<\/h2>\n<p>Before we talk about ads, playlists, and timing, you need a stable base: a fresh Next.js app, small UI layer, and Cloudinary ready to serve video.<\/p>\n<p>The goal for this section is simple:<\/p>\n<ul>\n<li>A new Next.js project running.<\/li>\n<li>\n<code>shadcn<\/code> wired in for UI.<\/li>\n<li>Cloudinary <code>env<\/code> set up with your main and ad videos.<\/li>\n<li>A blank <code>AdPlayer<\/code> on the home page, ready to evolve.<\/li>\n<\/ul>\n<h2>Create the Next.js App<\/h2>\n<p>Start with a standard App Router setup.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">npx<\/span> <span class=\"hljs-selector-tag\">create-next-app<\/span><span class=\"hljs-keyword\">@latest<\/span> ad-player\ncd ad-player\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><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>Use:<\/p>\n<ul>\n<li>TypeScript: yes<\/li>\n<li>Tailwind: yes<\/li>\n<li>App Router: yes<\/li>\n<\/ul>\n<p>You can run <code>npm run dev<\/code> now and see the default Next.js page at <code>http:\/\/localhost:3000<\/code>.<\/p>\n<p>This project in the repo lives here: <code>https:\/\/github.com\/musebe\/ad-player<\/code><\/p>\n<h2>Add <code>shadcn<\/code> for UI<\/h2>\n<p>You\u2019ll use <strong>shadcn<\/strong> to avoid hand rolling basic components.<\/p>\n<p>Initialize it:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">npx<\/span> <span class=\"hljs-selector-tag\">shadcn<\/span><span class=\"hljs-keyword\">@latest<\/span> init\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><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 pull in a few building blocks:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">npx<\/span> <span class=\"hljs-selector-tag\">shadcn<\/span><span class=\"hljs-keyword\">@latest<\/span> add card button input\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><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>These pieces give us the card for the player shell, the main play and skip buttons, and simple inputs for ad settings.<\/p>\n<h2>Wire Cloudinary via Environment Values<\/h2>\n<p>Next, you\u2019ll make sure the app knows where your videos live.<\/p>\n<p>Your <code>.env.local<\/code> is the contract between Next.js and Cloudinary.<\/p>\n<p>Create <code>.env.local<\/code> in the project root and add:<\/p>\n<pre class=\"js-syntax-highlighted\"><code>NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=demo-article-projects\n\n# Main videos\nNEXT_PUBLIC_MAIN_1=videos\/main\/my-main-video\nNEXT_PUBLIC_MAIN_2=videos\/main\/my-main-video2\nNEXT_PUBLIC_MAIN_3=videos\/main\/my-main-video3\n\n# Ads\nNEXT_PUBLIC_AD_1=videos\/ads\/my-ad\nNEXT_PUBLIC_AD_2=videos\/ads\/my-ad2\nNEXT_PUBLIC_AD_3=videos\/ads\/my-ad3\n\n<\/code><\/pre>\n<p>A few important points:<\/p>\n<ul>\n<li>\n<p>Each value is a <strong>public id<\/strong>, not a full URL.<\/p>\n<\/li>\n<li>\n<p>The folder layout in Cloudinary should match:<\/p>\n<ul>\n<li>\n<code>videos\/main\/my-main-video<\/code>\n<\/li>\n<li>\n<code>videos\/main\/my-main-video2<\/code>\n<\/li>\n<li>\n<code>videos\/main\/my-main-video3<\/code>\n<\/li>\n<\/ul>\n<p>and<\/p>\n<ul>\n<li>\n<code>videos\/ads\/my-ad<\/code>\n<\/li>\n<li>\n<code>videos\/ads\/my-ad2<\/code>\n<\/li>\n<li>\n<code>videos\/ads\/my-ad3<\/code>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>You\u2019ll turn these ids into real video URLs in one small helper.<\/p>\n<h2>Create a Cloudinary URL Helper<\/h2>\n<p>Instead of hard coding URLs everywhere, we use a single function that builds them using your <code>env<\/code> and <code>public id<\/code>.<\/p>\n<p>In <code>lib\/cloudinary.ts<\/code>:<\/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-comment\">\/\/ lib\/cloudinary.ts<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getCloudinaryVideoUrl<\/span>(<span class=\"hljs-params\">publicId: string<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;\n  <span class=\"hljs-keyword\">if<\/span> (!cloudName || !publicId) <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">''<\/span>;\n\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${cloudName}<\/span>\/video\/upload\/<span class=\"hljs-subst\">${publicId}<\/span>.mp4`<\/span>;\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The rest of the app only needs to know the id:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> url = getCloudinaryVideoUrl(<span class=\"hljs-string\">'videos\/main\/my-main-video'<\/span>);\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<blockquote>\n<p>You can check the full helper in the repo here: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/lib\/cloudinary.ts\"><code>lib\/cloudinary.ts<\/code><\/a><\/p>\n<\/blockquote>\n<h2>Add a Simple AdPlayer Shell<\/h2>\n<p>Now, you\u2019ll give the UI something to render. Nothing advanced yet, just a card that will host the player.<\/p>\n<p>In <code>components\/video\/AdPlayer.tsx<\/code>, the important part looks like:<\/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-string\">'use client'<\/span>;\n\n<span class=\"hljs-keyword\">import<\/span> { Card, CardHeader, CardTitle, CardContent } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@\/components\/ui\/card'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">AdPlayer<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Card<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">'w-full max-w-3xl mx-auto'<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CardHeader<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CardTitle<\/span>&gt;<\/span>Ad supported video player<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">CardTitle<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">CardHeader<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CardContent<\/span>&gt;<\/span>\n        {\/* Video, controls, and settings will live here *\/}\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">CardContent<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Card<\/span>&gt;<\/span><\/span>\n  );\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>Then, wire it into the home page:<\/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-comment\">\/\/ app\/page.tsx<\/span>\n<span class=\"hljs-keyword\">import<\/span> { AdPlayer } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@\/components\/video\/AdPlayer'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">HomePage<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">main<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">'min-h-screen flex items-center justify-center bg-background'<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">AdPlayer<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">main<\/span>&gt;<\/span><\/span>\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\">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>You can see the final full versions in the repo:<\/p>\n<ul>\n<li>\n<p><code>app\/page.tsx<\/code><\/p>\n<\/li>\n<li>\n<p><code>components\/video\/AdPlayer.tsx<\/code><\/p>\n<p>inside <code>https:\/\/github.com\/musebe\/ad-player<\/code><\/p>\n<\/li>\n<\/ul>\n<p>At this point, you\u2019ll have:<\/p>\n<ul>\n<li>A fresh Next.js app.<\/li>\n<li>\n<code>shadcn<\/code> wired in.<\/li>\n<li>Cloudinary <code>env<\/code> configured for three main videos and three ads.<\/li>\n<li>A centered <code>AdPlayer<\/code> card ready to host the video logic.<\/li>\n<\/ul>\n<h2>Wiring Cloudinary Into the Video Playlist<\/h2>\n<p>Now that the project runs, you\u2019ll connect it to Cloudinary. This gives the player real videos to work with.<\/p>\n<p>Your <code>.env.local<\/code> has six IDs: three main videos and three ads. You\u2019ll turn them into two simple lists: a <strong>content playlist<\/strong> and an <strong>ad list<\/strong>.<\/p>\n<h2>Create the Content Playlist<\/h2>\n<p>Inside <code>AdPlayer.tsx<\/code>, read the <code>env<\/code> values and convert each one into a Cloudinary URL using our small helper.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> mainIds = &#91;\n  process.env.NEXT_PUBLIC_MAIN_1,\n  process.env.NEXT_PUBLIC_MAIN_2,\n  process.env.NEXT_PUBLIC_MAIN_3,\n];\n\n<span class=\"hljs-keyword\">const<\/span> contentPlaylist = mainIds.map(<span class=\"hljs-function\">(<span class=\"hljs-params\">id, index<\/span>) =&gt;<\/span> ({\n  <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-string\">`video-<span class=\"hljs-subst\">${index + <span class=\"hljs-number\">1<\/span>}<\/span>`<\/span>,\n  <span class=\"hljs-attr\">title<\/span>: <span class=\"hljs-string\">`Main video <span class=\"hljs-subst\">${index + <span class=\"hljs-number\">1<\/span>}<\/span>`<\/span>,\n  <span class=\"hljs-attr\">src<\/span>: getCloudinaryVideoUrl(id),\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\">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>These items drive the sidebar playlist and give the player its active source.<\/p>\n<blockquote>\n<p>Full file: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/AdPlayer.tsx\">components\/video\/AdPlayer.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Build the Ad List<\/h2>\n<p>Ads are handled as a simple rotating set.<\/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> adIds = &#91;\n  process.env.NEXT_PUBLIC_AD_1,\n  process.env.NEXT_PUBLIC_AD_2,\n  process.env.NEXT_PUBLIC_AD_3,\n];\n\n<span class=\"hljs-keyword\">const<\/span> adList = adIds.map(<span class=\"hljs-function\">(<span class=\"hljs-params\">id<\/span>) =&gt;<\/span> getCloudinaryVideoUrl(id));\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>Later, you\u2019ll pick the current ad like this:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> &#91;adIndex, setAdIndex] = useState(<span class=\"hljs-number\">0<\/span>);\n<span class=\"hljs-keyword\">const<\/span> currentAdSrc = adList&#91;adIndex] || <span class=\"hljs-string\">''<\/span>;\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<h2>Track the Active Video<\/h2>\n<p>Choose which main item to show using simple state.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> &#91;currentIndex, setCurrentIndex] = useState(<span class=\"hljs-number\">0<\/span>);\n<span class=\"hljs-keyword\">const<\/span> currentItem = contentPlaylist&#91;currentIndex];\n<span class=\"hljs-keyword\">const<\/span> currentMainSrc = currentItem?.src || <span class=\"hljs-string\">''<\/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\">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 UI updates whenever you click a playlist item.<\/p>\n<blockquote>\n<p>Playlist component: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/VideoPlaylist.tsx\">components\/video\/VideoPlaylist.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Attach Everything to the Video Element<\/h2>\n<p>The <code>&lt;video&gt;<\/code> element lives in a separate component for clarity.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">\/\/ VideoSurface.tsx\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">video<\/span> <span class=\"hljs-attr\">ref<\/span>=<span class=\"hljs-string\">{videoRef}<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"h-full w-full object-cover\"<\/span> <span class=\"hljs-attr\">controls<\/span> \/&gt;<\/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\">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>You\u2019ll pass a ref from the player:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> videoRef = useRef&lt;HTMLVideoElement | <span class=\"hljs-literal\">null<\/span>&gt;(<span class=\"hljs-literal\">null<\/span>);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><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 ref lets you inject the video source, control playback, and listen for <code>timeupdate<\/code> and <code>ended<\/code> events.<\/p>\n<blockquote>\n<p>Video surface reference: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/VideoSurface.tsx\">components\/video\/VideoSurface.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Orchestrating the Ad Break<\/h2>\n<p>With our assets mapped and the playlist ready, the player now needs a sense of timing. It must know when to interrupt the show, how to deliver the ad, and how to guide the viewer back to their spot. This is where the actual behavior of the player comes to life.<\/p>\n<p>All of this logic stays in a single place:<\/p>\n<blockquote>\n<p>Full file: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/AdPlayer.tsx\"><code>components\/video\/AdPlayer.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>One Player, Two Personalities<\/h2>\n<p>Keep the app simple by using a <strong>single<\/strong> <code>&lt;video&gt;<\/code> element.<\/p>\n<p>Instead of juggling separate players for ads and content, you\u2019ll shift its behavior using one piece of state:<\/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;isPlayingAd, setIsPlayingAd] = useState(<span class=\"hljs-literal\">false<\/span>);\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><strong>When this flag is off<\/strong>, the player behaves like a normal content viewer.<\/p>\n<p><strong>When it turns on<\/strong>, the same element becomes an ad player.<\/p>\n<p>The UI only needs this flag to update labels like \u201cMain Video\u201d or \u201cAd\u201d, handled inside:<\/p>\n<blockquote>\n<p>Video surface: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/VideoSurface.tsx\"><code>components\/video\/VideoSurface.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>The Seamless Swap<\/h2>\n<p>The swap between content and ad needs to be invisible.<\/p>\n<p>Never replace the video element. Only change the <code>src<\/code> underneath it.<\/p>\n<p>Inside the effect that watches for mode changes, the player decides which URL to load:<\/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-keyword\">const<\/span> src = isPlayingAd ? currentAdSrc : currentMainSrc;\ncurrentVideo.src = src;\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>No flicker. No jump. The <code>&lt;video&gt;<\/code> tag stays rooted in place and simply loads its new source.<\/p>\n<h3>The Interruption, Right on Time<\/h3>\n<p>To interrupt the main video at the right second, you\u2019ll use its natural timeline. The video fires a <code>timeupdate<\/code> event many times per second. Watch that and compare it to the chosen trigger time:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-16\" 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> (video.currentTime &gt;= adStartTime) {\n  setResumeTime(video.currentTime);\n  setIsPlayingAd(<span class=\"hljs-literal\">true<\/span>);\n  setHasPlayedAd(<span class=\"hljs-literal\">true<\/span>);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><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 does three things at once:<\/p>\n<ul>\n<li>\n<strong>Bookmarks<\/strong> the exact second the viewer reached.<\/li>\n<li>\n<strong>Switches<\/strong> the player into ad mode.<\/li>\n<li>\n<strong>Prevent repeats<\/strong> by marking the ad as \u201cplayed\u201d for this video.<\/li>\n<\/ul>\n<p>The controls for setting the trigger time live here:<\/p>\n<blockquote>\n<p>Ad settings: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/AdConfigPanel.tsx\"><code>components\/video\/AdConfigPanel.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>The Return Journey<\/h2>\n<p>Finishing a video clip, whether it\u2019s the ad or the main content, signals what happens next.<\/p>\n<p>Listen to the <code>ended<\/code> event and make a choice.<\/p>\n<p>When an <strong>ad<\/strong> ends:<\/p>\n<ul>\n<li>Switch back to content.<\/li>\n<li>Restore where the viewer left.<\/li>\n<li>Advance the ad index so the next video gets a different ad.<\/li>\n<\/ul>\n<p>When the <strong>content<\/strong> ends:<\/p>\n<ul>\n<li>Reset ad state.<\/li>\n<li>Move to the next item in the playlist.<\/li>\n<\/ul>\n<p>The core logic looks like:<\/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\">if<\/span> (isPlayingAd) {\n  setIsPlayingAd(<span class=\"hljs-literal\">false<\/span>);\n  setAdIndex(<span class=\"hljs-function\">(<span class=\"hljs-params\">prev<\/span>) =&gt;<\/span> (prev + <span class=\"hljs-number\">1<\/span>) % adList.length);\n} <span class=\"hljs-keyword\">else<\/span> {\n  setCurrentIndex(<span class=\"hljs-function\">(<span class=\"hljs-params\">prev<\/span>) =&gt;<\/span> prev + <span class=\"hljs-number\">1<\/span>);\n}\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>The playlist UI itself lives here:<\/p>\n<blockquote>\n<p>Playlist view: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/VideoPlaylist.tsx\"><code>components\/video\/VideoPlaylist.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>Controls for Developers and Viewers<\/h2>\n<p>Sometimes you\u2019ll want to test behavior instantly without waiting for the trigger time. Sometimes the viewer wants to skip the ad. Both needs are covered.<\/p>\n<p>The control panel gives you:<\/p>\n<ul>\n<li>Play<\/li>\n<li>Skip ad<\/li>\n<li>Force ad<\/li>\n<li>Apply new settings (resetting the state cleanly)<\/li>\n<\/ul>\n<p>Here\u2019s the reset logic:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-18\" 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\">handleApplySettings<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  setIsPlayingAd(<span class=\"hljs-literal\">false<\/span>);\n  setResumeTime(<span class=\"hljs-number\">0<\/span>);\n  currentVideo.currentTime = <span class=\"hljs-number\">0<\/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\">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 control toolbar is here:<\/p>\n<blockquote>\n<p>Player controls: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/ControlsBar.tsx\"><code>components\/video\/ControlsBar.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<p>This completes the core loop that makes the player feel dynamic and intentional:<\/p>\n<p><strong>Watch \u2192 Interrupt \u2192 Show Ad \u2192 Resume \u2192 Next Video.<\/strong><\/p>\n<h2>Building the Playlist Experience<\/h2>\n<p>A single video with an ad break is a start, but a real platform needs a queue. The player needs a simple way to \u201cchange the channel\u201d when a video ends or when the viewer picks a new one.<\/p>\n<p>The playlist logic sits beside the player state so the core video logic stays focused on one thing, playing the current frame.<\/p>\n<blockquote>\n<p>Full file: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/VideoPlaylist.tsx\"><code>components\/video\/VideoPlaylist.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>The Data Structure<\/h2>\n<p>The playlist stays small and predictable. It\u2019s just an array of objects with three fields: id, title, and a Cloudinary URL. The player never looks at the full list during playback; it only needs the <code>currentIndex<\/code>.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">type PlaylistItem = {\n  id: string;\n  title: string;\n  src: string;\n};\n<\/code><\/span><\/pre>\n<p>To keep track of where the viewer is:<\/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\">const<\/span> &#91;currentIndex, setCurrentIndex] = useState(<span class=\"hljs-number\">0<\/span>);\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>Changing <code>currentIndex<\/code> updates <code>currentMainSrc<\/code>, and the <code>&lt;video&gt;<\/code> element loads the proper source automatically.<\/p>\n<h2>The Clean Slate (Navigation)<\/h2>\n<p>This part matters the most. Switching videos isn\u2019t enough.<\/p>\n<p>You\u2019ll have to clear any leftover ad state so the next video behaves like a fresh start.<\/p>\n<p>When the user clicks a playlist item, you\u2019ll reset the internal ad markers:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-20\" 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\">handleSelectPlaylistIndex<\/span>(<span class=\"hljs-params\">i<\/span>) <\/span>{\n  setCurrentIndex(i);\n  setHasPlayedAd(<span class=\"hljs-literal\">false<\/span>);\n  setIsPlayingAd(<span class=\"hljs-literal\">false<\/span>);\n  setResumeTime(<span class=\"hljs-number\">0<\/span>);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><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 prevents the new video from skipping its ad break or jumping to an old timestamp.<\/p>\n<p>The player now treats the selection as brand new content.<\/p>\n<h3>The Binge Factor (Auto-Advance)<\/h3>\n<p>A playlist should feel relaxing. When a video ends, the player checks if there\u2019s another item lined up. If yes, it moves forward. If not, it stays at the last item.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">setCurrentIndex(<span class=\"hljs-function\">(<span class=\"hljs-params\">prev<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> next = prev + <span class=\"hljs-number\">1<\/span>;\n  <span class=\"hljs-keyword\">return<\/span> next &lt; contentPlaylist.length ? next : prev;\n});\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><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 simple step creates a smooth flow:<\/p>\n<ol>\n<li>Video ends.<\/li>\n<li>Ad state resets.<\/li>\n<li>Next video loads.<\/li>\n<li>A new ad break waits at its timestamp.<\/li>\n<\/ol>\n<h2>The UI Component<\/h2>\n<p>The playlist UI is intentionally simple. It shows a list, highlights the active item, and reports back which index was clicked.<\/p>\n<p>It doesn\u2019t manage logic or touch ad state. Rather, it stays \u201cdumb\u201d, which keeps your system predictable.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-22\" 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\">li<\/span> <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{item.id}<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> onSelect(index)}&gt;\n    {item.title}\n    {isActive &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span>&gt;<\/span>Now Playing<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span>}\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">li<\/span>&gt;<\/span>\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><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>Since both a user click and auto-advance use the same callback, they both trigger the same reset flow. Every transition behaves consistently.<\/p>\n<blockquote>\n<p>You can review the full playlist component in: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/VideoPlaylist.tsx\"><code>components\/video\/VideoPlaylist.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>Showing the Right Interface for the Moment<\/h2>\n<p>A player shifts roles as people use it. One second it shows the main story. In the next, it becomes an ad frame. Sometimes, it becomes a testing tool.<\/p>\n<p>To keep things clear, each part of the UI has a single job. Each component reacts to the state the player controls.<\/p>\n<blockquote>\n<p>Full file: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/AdPlayer.tsx\"><code>components\/video\/AdPlayer.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>Visual Feedback With the Video Surface<\/h2>\n<p>The <code>&lt;VideoSurface&gt;<\/code> component only cares about two things.<\/p>\n<p>It needs the video ref and a simple flag that tells it whether the screen is in ad mode or main mode. This lets it update the badge, borders, or any other visual hint without touching playback logic.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-23\" 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\">VideoSurface<\/span> <span class=\"hljs-attr\">videoRef<\/span>=<span class=\"hljs-string\">{videoRef}<\/span> <span class=\"hljs-attr\">isPlayingAd<\/span>=<span class=\"hljs-string\">{isPlayingAd}<\/span> \/&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><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<blockquote>\n<p>Full component: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/VideoSurface.tsx\"><code>components\/video\/VideoSurface.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>Capturing User Intent Through the Control Bar<\/h2>\n<p>The Control Bar listens to the viewer (play, skip, interact). It doesn\u2019t run the logic itself. It forwards intent to the parent player.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-24\" 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\">ControlsBar<\/span>\n  <span class=\"hljs-attr\">onPlay<\/span>=<span class=\"hljs-string\">{handlePlay}<\/span>\n  <span class=\"hljs-attr\">onSkipAd<\/span>=<span class=\"hljs-string\">{handleSkipAd}<\/span>\n  <span class=\"hljs-attr\">isPlayingAd<\/span>=<span class=\"hljs-string\">{isPlayingAd}<\/span>\n\/&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The loop stays simple.<\/p>\n<p>You click, the parent updates state, the UI responds.<\/p>\n<blockquote>\n<p>Full component: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/ControlsBar.tsx\"><code>components\/video\/ControlsBar.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>Adjusting Behavior With the Config Panel<\/h2>\n<p>This panel is the testing ground. You\u2019ll set the ad start time and change the ad URL.<\/p>\n<p>Press <strong>Apply<\/strong>, and the player resets itself and begins again with fresh settings.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-25\" 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\">AdConfigPanel<\/span>\n  <span class=\"hljs-attr\">adStartTime<\/span>=<span class=\"hljs-string\">{adStartTime}<\/span>\n  <span class=\"hljs-attr\">onAdStartTimeChange<\/span>=<span class=\"hljs-string\">{setAdStartTime}<\/span>\n  <span class=\"hljs-attr\">onApply<\/span>=<span class=\"hljs-string\">{handleApplySettings}<\/span>\n\/&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><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>It\u2019s a quiet, focused tool that helps you shape and test the ad experience.<\/p>\n<blockquote>\n<p>Full component: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/AdConfigPanel.tsx\"><code>components\/video\/AdConfigPanel.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h3>Showing Context With the Header<\/h3>\n<p>The header sets the tone. If the viewer is watching the main content, it shows the title. If an ad is playing, it offers a link that opens the advertiser\u2019s page in a new tab.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-26\" 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\">AdPlayerHeader<\/span> <span class=\"hljs-attr\">isPlayingAd<\/span>=<span class=\"hljs-string\">{isPlayingAd}<\/span> <span class=\"hljs-attr\">onVisitAd<\/span>=<span class=\"hljs-string\">{handleOpenAd}<\/span> \/&gt;<\/span>\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><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>A small detail, but one that keeps the viewer informed.<\/p>\n<blockquote>\n<p>Full component: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/AdPlayerHeader.tsx\"><code>components\/video\/AdPlayerHeader.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>Putting It All Together<\/h2>\n<p>At this point, every part of the system knows its job. The playlist holds the data, the UI shows the current state, and the controls capture what the viewer wants to do.<\/p>\n<p>The <code>AdPlayer<\/code> component is the conductor. It keeps everything in sync. Instead of many wrappers, the main logic lives in one place.<\/p>\n<blockquote>\n<p>Full file: <a href=\"https:\/\/github.com\/musebe\/ad-player\/blob\/main\/components\/video\/AdPlayer.tsx\"><code>components\/video\/AdPlayer.tsx<\/code><\/a><\/p>\n<\/blockquote>\n<h2>Your Video Playlist Flow<\/h2>\n<ol>\n<li>\n<p><strong>A main video loads.<\/strong> The player reads the <code>currentIndex<\/code> from the playlist and feeds that Cloudinary URL to the video element. The surface displays it without delay.<\/p>\n<\/li>\n<li>\n<p><strong>Playback starts.<\/strong> The player listens for <code>timeupdate<\/code>. Nothing special happens yet. The state is clean, the ad has not played, and the viewer settles in.<\/p>\n<\/li>\n<li>\n<p><strong>The trigger hits.<\/strong> When the current time crosses the <code>adStartTime<\/code>, the player bookmarks the exact second (<code>resumeTime<\/code>), switches the <code>isPlayingAd<\/code> flag to true, and replaces the source. The frame never flickers.<\/p>\n<\/li>\n<li>\n<p><strong>The ad runs to completion.<\/strong> Once the ad ends, the player rotates the ad queue and returns to the main video, placing the viewer exactly where they left off.<\/p>\n<\/li>\n<li>\n<p><strong>The content continues.<\/strong> If the content reaches its end, the player automatically moves to the next playlist item. The ad state resets (<code>hasPlayedAd = false<\/code>) so the next video can fire its ad at the right moment.<\/p>\n<\/li>\n<li>\n<p><strong>The cycle repeats.<\/strong> Across all main videos, the state machine behaves the same. You can jump between items in the playlist, and the player always resets the logic so each video gets its own clean slate.<\/p>\n<\/li>\n<\/ol>\n<h2>Where to Go From Here?<\/h2>\n<ul>\n<li>Live app: <a href=\"https:\/\/ad-player-rho.vercel.app\/\">https:\/\/ad-player-rho.vercel.app\/<\/a>\n<\/li>\n<li>Repo: <a href=\"https:\/\/github.com\/musebe\/ad-player\">https:\/\/github.com\/musebe\/ad-player<\/a>\n<\/li>\n<\/ul>\n<p>You can now play a main video, insert an ad mid-stream, resume from the right second, and move through a playlist. All of it is driven by simple React state, environment variables, and Cloudinary URLs.<\/p>\n<p>Here are some ideas on how to make your video player even more advanced:<\/p>\n<ol>\n<li>\n<strong>Add more types of ad breaks.<\/strong> Right now, the player supports a single mid-roll per video. You can extend the same pattern to handle:<\/li>\n<\/ol>\n<ul>\n<li>Pre-roll ads that run before the main content.<\/li>\n<li>Post-roll ads that play after the main video ends.<\/li>\n<li>Multiple mid-roll breaks using an array of trigger times.<\/li>\n<\/ul>\n<p>Each of these can reuse the same <code>isPlayingAd<\/code>, <code>resumeTime<\/code>, and <code>hasPlayedAd<\/code> pattern, just with more structured data.<\/p>\n<ol start=\"2\">\n<li>\n<strong>Make ad logic server-driven.<\/strong> At the moment, the player reads its ad config from <code>.env<\/code>. For a real product, you might move this to a backend or CMS.<\/li>\n<\/ol>\n<p>Ideas:<\/p>\n<ul>\n<li>Expose an API route that returns ad rules per video id.<\/li>\n<li>Store ad schedules, links, and targeting in a database.<\/li>\n<li>Use Next.js Server Components to fetch config and feed it into <code>AdPlayer<\/code> as props.<\/li>\n<\/ul>\n<p>This keeps the frontend clean while giving you full control over campaigns.<\/p>\n<ol start=\"3\">\n<li>\n<strong>Use more Cloudinary Video features.<\/strong> Right now, the player uses Cloudinary as a reliable video host. You can also lean into its media features.<\/li>\n<\/ol>\n<p>For example:<\/p>\n<ul>\n<li>Generate different transformations per device or network quality.<\/li>\n<li>Use Cloudinary\u2019s video player or widget for analytics and advanced controls.<\/li>\n<li>Tag assets to group by campaign, category, or region.<\/li>\n<\/ul>\n<p>If you already organize your media library in folders like <code>videos\/main<\/code> and <code>videos\/ads<\/code>, you are one step away from building dynamic playlists from Cloudinary\u2019s Admin API.<\/p>\n<ol start=\"4\">\n<li>\n<strong>Track and test performance.<\/strong> Because the <code>AdPlayer<\/code> has a clear state, it is easy to wire in analytics.<\/li>\n<\/ol>\n<p>You can log events, such as when an ad starts and finishes, which ad ran before which content, when users skip, and more. Over time, they help you tune how long your ad runs and its placements.<\/p>\n<ol start=\"5\">\n<li>\n<strong>Turn the pattern into a reusable module.<\/strong> The current code lives inside this one project, but the structure is reusable.<\/li>\n<\/ol>\n<p>You can:<\/p>\n<ul>\n<li>Extract <code>AdPlayer<\/code> and related components into a separate internal package.<\/li>\n<li>Wrap it in a simple <code>&lt;AdPlaylistPlayer \/&gt;<\/code> component that accepts props like <code>items<\/code>, <code>ads<\/code>, and <code>defaultAdStartTime<\/code>.<\/li>\n<li>Drop it into other Next.js projects that also use Cloudinary.<\/li>\n<\/ul>\n<p>That way, \u201cad-supported playlist\u201d becomes a building block in your stack, not a one-off experiment. Ready to get started? <a href=\"https:\/\/cloudinary.com\/users\/register_free\">Sign up<\/a> for a free Cloudinary account today.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":39594,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[212,303,310],"class_list":["post-39593","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-next-js","tag-video","tag-video-player"],"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>Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary<\/title>\n<meta name=\"description\" content=\"Learn how to build a simple video player that plays multiple videos and mid-roll ads for a &quot;stream-like&quot; experience.\" \/>\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\/dynamic-ad-supported-video-playlist-next-js\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary\" \/>\n<meta property=\"og:description\" content=\"Learn how to build a simple video player that plays multiple videos and mid-roll ads for a &quot;stream-like&quot; experience.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-12-17T15:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"2000\" \/>\n\t<meta property=\"og:image:height\" content=\"1100\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\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\/dynamic-ad-supported-video-playlist-next-js#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary\",\"datePublished\":\"2025-12-17T15:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js\"},\"wordCount\":9,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA\",\"keywords\":[\"Next.js\",\"Video\",\"Video Player\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2025\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js\",\"url\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js\",\"name\":\"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA\",\"datePublished\":\"2025-12-17T15:00:00+00:00\",\"description\":\"Learn how to build a simple video player that plays multiple videos and mid-roll ads for a \\\"stream-like\\\" experience.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\",\"name\":\"melindapham\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"caption\":\"melindapham\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary","description":"Learn how to build a simple video player that plays multiple videos and mid-roll ads for a \"stream-like\" experience.","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\/dynamic-ad-supported-video-playlist-next-js","og_locale":"en_US","og_type":"article","og_title":"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary","og_description":"Learn how to build a simple video player that plays multiple videos and mid-roll ads for a \"stream-like\" experience.","og_url":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js","og_site_name":"Cloudinary Blog","article_published_time":"2025-12-17T15:00:00+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA","type":"image\/jpeg"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary","datePublished":"2025-12-17T15:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js"},"wordCount":9,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA","keywords":["Next.js","Video","Video Player"],"inLanguage":"en-US","copyrightYear":"2025","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js","url":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js","name":"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA","datePublished":"2025-12-17T15:00:00+00:00","description":"Learn how to build a simple video player that plays multiple videos and mid-roll ads for a \"stream-like\" experience.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/dynamic-ad-supported-video-playlist-next-js#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Dynamic Ad-Supported Video Playlist With Next.js and Cloudinary"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9","name":"melindapham","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","caption":"melindapham"}}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1764964594\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary\/Blog_Building_a_Dynamic_Ad-Supported_Video_Playlist_with_Next.js_and_Cloudinary.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39593","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\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=39593"}],"version-history":[{"count":1,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39593\/revisions"}],"predecessor-version":[{"id":39595,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39593\/revisions\/39595"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/39594"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=39593"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=39593"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=39593"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}