When Managing Toddler Screen Time Requires Reverse Engineering
Imagining what attributes a child-oriented streaming service could have (if you're eccentric)
Part of my on-going "doing strange things to wrestle back control of technology" series.
Motivation
There is no more user hostile product online than streaming services, especially in the context of managing the screen time of children.
All streaming services are intentionally designed to be as aggravating as possible, while keeping you (or more importantly your children) engaged, trudging endlessly through content carousels. We're so used to this state of affairs I don't think many people give it much consideration.
If you were to imagine from scratch what attributes a child-oriented streaming service would or should have you might think of the following:
- An allowlist by default vs. a blocklist: Blocklists are a feature I am only aware of on Netflix (I know Disney does not have one), though you have to login to the site directly in a browser to access it, and you have to enter one show at a time. I should not have to manually remove dozens of programs from my child's profile individually.
- Timers: You should be able to set timeouts with various fun, granular options for scheduling blackouts and whatnot.
- More than one watchlist: e.g., like song playlists.
- Age blocks: These are at least available universally
- No ads, suggestions, nudges, etc. in children's profiles
The most control I've managed to exert over our daughter's streaming setup is to make one curated watchlist on each app, and single out Cocomelon for being entirely struck from Netflix. Even so, to get to the watchlist I have to march past Disney's latest content offerings first, i.e., 😲 "what's that one??"
Is there a better way?
The better way
Initial Proposal
I originally had the idea of creating some kind of physical show and movie "tokens" my three year old daughter could use to choose her own shows in a responsible and controlled manner.
Roku did not make this straightforward.
Ideally, this system would be smart enough to play only one show or movie at a time, but this proved to be infeasible for several reasons:
- My original draft idea was to create a fixed barcode scanner driven setup to scan QR codes linked to content and start shows and movies. I eventually ruled this out because I don't like lasers, even low powered barcode scanners, being used around toddlers, and rigging up a Raspberry Pi and webcam for image recognition was too much added work. I did keep the QR code concept in part.
There is no way to randomly choose an episode.
- There is no way to guarantee a show or movie will play from the beginning if it was already started once before. This is exceedingly annoying, and something I personally can't stand on my own viewings. In my opinion, shows and movies should always start from the beginning, and have a button to pick back up at the last position. Without this, coding my own automatic screen time limits per episode would not be not reliable.
Revised Proposal
Scaling back my original ambitions I settled on creating QR code cards that will directly play a show or movie on the correct service when scanned by your phone. This is still a major improvement in that the watchlist has now become only what is physically present, and by exchanging a card to be scanned and then shelved the illusion that streaming is something limitless is eroded a little.
Even this was difficult.
Exploring Roku APIs
So what can you do with them?
I spent an afternoon pouring over various Roku API documentation, and it became clear they also are not very invested in you having a lot of control over how you consume content. Less so than the streaming companies, likely because Roku is here for selling your data to advertisers, and probably isn't that invested in which particular channel you're watching, but still generally somewhat hostile.
Roku devices all run a local REST API you can easily access, and for many things this is what the mobile app is using. The External Control Protocol (ECP) is charming, but basically doesn't expose any different functionality than just using the remote. I experimented with chaining together commands to use the on-screen Roku-side search to play a show or movie, but for some reason using ECP to access the on-device search was, at least on my TCL TV model, completely broken.
It turns out Roku's Deep Linking API is what I was after, there was just one minor roadblock.
The Deep Linking API, while technically accessible to everyone, seems to be mainly intended for Roku channel developers, and the contentId
of a show or movie (which is what uniquely identifies a program) is more or less "secret," or perhaps more charitably "undocumented." The API documentation presumes you know your own program's contentId
and therefore does not expose it in any other API methods. I found very few references on this subject online, aside from the helpful observations by a few people that both Hulu and YouTube just use obvious URL parameters you can easily discover by just watching something within a browser.
Disney Plus (which is identified on Rokus as channel 291097
) was the channel I was focusing on first, and I was initially confused by their most probable contentId
scheme, as the GUID in the URL was the most obvious choice, and it did not work the first round of testing I did.
Both of these examples simply pulled up the Disney Plus landing page.
curl -X POST "http://[roku_ip_address]:8060/launch/291097?contentId=entity-fa6973b9-e7cf-49fb-81a2-d4908e4bf694"
curl -X POST "http://[roku_ip_address]:8060/launch/291097?contentId=fa6973b9-e7cf-49fb-81a2-d4908e4bf694"
I reasoned though that the mobile app must be using this API for kicking off a show when selecting the Disney Plus option in the in-app search, so I setup a packet trace logger on my phone and fired off an episode of Bluey.
Glancing at one of the packets I then sighed.
GET /ecp-session HTTP/1.1
Sec-WebSocket-Origin: Android
Sec-WebSocket-Protocol: ecp-2
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: permessage-deflate
Host: xxx.xxx.xx.xxx:8060
Accept-Encoding: gzip
User-Agent: okhttp/4.9.0-roku1
It's using encrypted WebSockets for local requests. I conceptually understand WebSockets but was not experienced in debugging them, or network packets in general to be honest, much less manually figuring out the key exchange, but I decided to fire up Wireshark for the first time in a decade and see if this is something it can seamlessly parse.
It turns out it can. I selected the "Unmasked data" button and I was greeted with the following request:
I first I thought this was the GUID for Bluey, as the mediaType
was labeled as "series" and Bluey started playing immediately, so I proclaimed success, wrote and published the first draft of this post, and went to bed.
Day 2
Assumptions were hasty
I have an unfortunate habit of making hasty assumptions, and decided to Google that GUID the next morning, as surely some tired tech parent had to have done this before me with Bluey, and I was curious what I would find.
The GUID 133b4d69-2863-4713-a6a4-62012caf4759
does not actually belong to Bluey, it belongs to Season 1 Episode 52, "Verandah Santa," which is in fact what played the day prior.
Where I got tripped up is that on my TCL Roku TV, besides the packet data above being a bit misleading, the mediaType
also did not seem especially important at the time, and the Attempt #1
cURL example above did indeed start playing a Bluey episode so I assumed it worked as expected. This transaction however does not work on my Roku Streaming Stick and drops you back into the Disney landing screen. When you add mediaType=episode
it now works as expected on both devices, if Verandah Santa is what you're after.
When I did the initial packet trace I did also try to setup a MITM as well, but the Roku app used certificate pinning and would not function until I turned it off, so I could not see any intermediate request that would have indicated what the GUID was of Bluey itself. My guess is that the app pulled down the contentId
of the next episode of Bluey from an external server, and then that is what I captured later when it was sent down to the Roku.
Now that I realized that perhaps the mediaType
is not so optional after all, and knowing that the GUIDs for episodes are not actually obfuscated on Disney dot com I revisited the URL for the series page, dropped the entity-
prefix, and this time added mediaType=series
to the request.
Actual success. To directly play a show skipping every menu and layer, you simply need to POST
to the Roku's API with the parameters above.
Next I moved on to making a physical show card. To my knowledge there is no way to indicate POST
vs. GET
from a QR code, so I did still need to create a shim of some kind to tie everything together. I made a very basic Flask script to send a GET
to via a QR code, and which then generates the POST
to the Roku's API.
From there it's just a matter of printing and laminating.
And it works! Simply scanning the QR code with your phone automatically begins playing an episode of Bluey on the living room TV.
Now that we know that the GUIDs in the URL strings actually do point to individual episodes it is trivial to scrape a list of them from the show page and build a local catalog. This is nice as well because if there are particular episodes you'd like to avoid you can exclude them.
<a
role="link"
data-item-id="c578bc71-4814-492c-bd40-dbbd5e31a45a"
class="_2c2l371 _11x60sa1 xgfbc1xq xgfbc117a xgfbc11gu xgfbc11qe xgfbc141i xgfbc1c5 xgfbc1d2"
data-testid="set-item"
aria-label="Season 1 Episode 43 Camping. 100 percent complete"
aria-disabled="false"
tabindex="0"
href="/play/c578bc71-4814-492c-bd40-dbbd5e31a45a"
>
As a Phase 2 I will gather a list of episodes similar to Mr. ragingcomputer from Github earlier, and extend the Flask script to randomly choose a recently un-watched episode, maintain a log of the past viewings, print more cards, and start utilizing the chore timer for pacing TV time.
I thought it would also be cute to purchase some cassette tape jewel cases and make mini VHS style covers with the QR codes inside that can sit on the shelf and recapture the vibe of watching movies 20 years ago. That project will be an upcoming post.
Happy hacking.
Post Revision Log
I often make mistakes. As they come to my attention I will post revisions.
- 2024-08-19: I initially thought the GUID
133b4d69-2863-4713-a6a4-62012caf4759
belonged to Bluey the show since that was what I selected on the remote app. It actually belongs to Season 1 Episode 52, "Verandah Santa." I've revised the post. - 2024-08-21: Minor edits