DEV Community

Yuriy Stasyuk
Yuriy Stasyuk

Posted on

Things That Bit Me Wrapping the YouTube IFrame API for a Kids' App

I built WiseTube because the YouTube sidebar kept leading my kids somewhere I didn't want them. It's a free library of hand-picked learning videos that play inside my own player, with YouTube's suggestions, Shorts and autoplay-to-the-next-thing taken out.

The covering-up part gets most of the attention. This post is about the smaller stuff that cost me evenings. If you're wrapping the IFrame API for anything, some of these will probably find you too.

The phone view, where a kid actually taps all this.

1. The API script can hang, so give it a deadline

You load https://www.youtube.com/iframe_api and wait for onYouTubeIframeAPIReady. Most of the time it's fine. On a flaky connection it can sit there forever, and a kid staring at a spinner will go find another app.

I wrap the load in a promise, cache it so it only happens once, and clear the cache if the script errors so the next try starts clean. The player also starts a 15 second timer. If onReady hasn't fired by then, it shows "The film didn't load" with a retry button.

2. YouTube resets your playback speed

I save speed and volume to localStorage so a kid who likes 1.25x doesn't have to set it every time. I set the rate in onReady, and that isn't enough, because YouTube can reset the rate when a film starts playing. The fix is boring. On every PLAYING state change, read getPlaybackRate() and set it again if it doesn't match the saved value.

3. Autoplay for the chosen film still gets blocked sometimes

I want the film a kid picked to start right away, so the player is created with autoplay: 1. Browsers don't always allow it. There's no clean event for "autoplay was blocked," so after onReady I wait about 1.2 seconds and check the state. If it's still UNSTARTED or CUED, I show my own big play button.

That's the only autoplay in the app. When a film ends, nothing else starts on its own.

4. Don't let callbacks restart the player

The Player component takes onTick, onEnded and onError from the page. If those are effect dependencies, any parent re-render can tear down the iframe and build a new one mid-film. I use React's useEffectEvent for them, so the effect that mounts the player only depends on the video id, the retry count and the title.

Progress gets read every 250ms for the scrub bar but only reported up every 5 seconds. Saving progress four times a second felt silly.

5. Validate pasted links before you trust them

Parents can add a film by pasting a YouTube link. People paste everything: youtu.be links, /embed/ links, watch?v= links, Shorts. I parse the URL, accept the normal shapes, and reject anything under /shorts/ or /live/ with "Paste a normal YouTube link. Shorts stay out of WiseTube."

Then I call YouTube's oEmbed endpoint for the link. If it doesn't come back with a title and channel name, the film doesn't get added. That catches typos, private videos and videos that can't be embedded, before a kid ever taps them.

6. A PIN on a device with no server

There are no accounts. Profiles, progress and settings sit in localStorage. The four-digit parent PIN is stored as a SHA-256 hash with a random 16-byte salt, using crypto.subtle.

I'll be straight about what that buys. A four-digit PIN on a device the kid holds is a speed bump. It keeps a six-year-old out of the settings. A teenager with dev tools could get past it. For a free app with no backend, I'm fine with that, and I'd rather say it than pretend it's a vault.

Because everything is on one device, the parent settings have an export button that downloads a wisetube-library.json with profiles, PIN, added and hidden films, and progress. Import shows how many learners are in the file and asks before replacing anything.

The little-kids look. The PIN keeps them in this lane.

7. Vite preview lies about routing

The site is a static build on Cloudflare Pages. Public pages are prerendered for search. Private app routes like /watch/:youtubeId and /parent are rewritten to a noindex app.html through public/_redirects, so a refresh doesn't 404.

vite preview ignores _redirects and Pages' 404 behavior, so a route can look fine locally and behave differently once deployed. I test routing with npx wrangler pages dev dist, which behaves like the real thing.

8. Tests for the catalog, too

The video list is data, and data drifts. npm run check runs TypeScript, oxlint and vitest, and the catalog tests fail if an import sneaks in a Short, a duplicate, an unknown topic, or a channel I already ruled out. That has caught more mistakes than any of my UI tests.

You can try the result at wisetube.yuriystasyuk.com. If you've wrapped the IFrame API and found a cleaner way around any of these, I'd love to hear it.

Top comments (0)