Child-Friendly Media Player

The Problem

I have a kid. She likes watching shows. Those shows are not necessarily easy to play to a screen without controlling the system through a phone/tablet/whatever. I want to give her autonomy over choosing what to watch during her allowed time without giving her a second, smaller screen to control the bigger screen.

We discussed a DVD player, but DVD menus are useless for someone who can’t read yet and DVDs are notoriously easy for kids to destroy. We also ran into the issue of a lot of shows or movies not being on physical media.

I was looking for a solution that had these requirements:

  • Whitelist for allowed media
  • Controllable by a child
  • Withstand use by a child
  • No menu navigation
  • Easily add or remove content (as shows get new episodes or we get new movies)

After dozens of seconds of searching for a solution, I decided to make my own.

player

The Solution

The core solution was based on a NFC Hat for a raspberry pi, a big pile of 25mm circle NFC tags, and some sticker paper.

We went to the library with some sticker paper and a bunch of images on a thumb drive and used their sticker-making software to print out two stickers for each show and attached them to tokens at random. tokens

After putting the stickers on the tokens, I used the demo software that came with the NFC Hat to get the token UID for each one and noted down which token ID went with which show. These UIDs and names made it into a config file that looks something like this:

Elinor|4,177,151,158,36,2,137|elinor-wonders-why
Toy Story 2|4,33,155,60,36,2,137|/mnt/Media/Kids Movies/Toy Story 2 (1999).mp4
Wishbone|4,81,32,160,36,2,137|/mnt/Media/Kids TV/Wishbone (1995)/

The Technical Bits

The shows and movies that we wanted available break down into three categories:

  • PBS Kids shows that are still on the air
  • PBS shows that are off air
  • Movies that we’ve ripped onto a local server

PBS Kids API

For the first (and most important) category, we need to take a look at the PBS Kids API. You can access a list of currently running shows using their show-tracking endpoint. That endpoint will show you a blob of JSON that contains the title and a “slug” for each show. Take that slug and swap it into the shows parameter on their show-list endpoint and you can see the list of available episodes for a given show.

Some example python to grab a video link for a given stub:

def get_url(stub):
    url = f"https://producerplayer.services.pbskids.org/show-list?shows={stub}&page=1&page_size=50&available=public&type=full_length"
    response = requests.get(url).json()
    randomepisodenumber = random.randint(0,len(response["items"])-1)
    print(randomepisodenumber)
    episodeurl=response["items"][randomepisodenumber]["videos"][0]["url"]
    print(episodeurl)
    return episodeurl

Off-Air PBS shows

We have plenty of off-air PBS shows, too. Wishbone, Magic School Bus, Between the Lions, etc. For these shows, I have a network location set as the matching configuration and do a walk of the directory and pick a random episode from the pile.

if(os.path.isdir(split[2].rstrip())):
    files = [os.path.join(path, filename)
        for path, dirs, files in os.walk(split[2].rstrip())
        for filename in files
        if filename.endswith(".mp4") or filename.endswith(".m4v")]
    return random.choice(files)

Local Movies

This one is pretty straightforward, if the mapping config file links to a specific file, just play the specific file

if(os.path.isfile(split[2].rstrip())):
    return split[2].rstrip()
Full python file
import RPi.GPIO as GPIO
import time
import subprocess
import os.path
import random
import requests

from pn532 import *

def get_url(stub):
    url = f"https://producerplayer.services.pbskids.org/show-list?shows={stub}&page=1&page_size=50&available=public&type=full_length"
    response = requests.get(url).json()
    randomepisodenumber = random.randint(0,len(response["items"])-1)
    print(randomepisodenumber)
    episodeurl=response["items"][randomepisodenumber]["videos"][0]["url"]
    print(episodeurl)
    return episodeurl

def get_file(tokenUid = []):
    with open("/mnt/Media/NFCPlayer/map.txt") as file:
        while line := file.readline():
            split = line.split('|')
            numbers = [int(numeric_string) for numeric_string in split[1].split(',')]
            if(numbers == tokenUid):
                if(os.path.isdir(split[2].rstrip())):
                    files = [os.path.join(path, filename)
                        for path, dirs, files in os.walk(split[2].rstrip())
                        for filename in files
                        if filename.endswith(".mp4") or filename.endswith(".m4v")]
                    return random.choice(files)
                elif(os.path.isfile(split[2].rstrip())):
                    return split[2].rstrip()
                else:
                    return get_url(split[2].rstrip())

if __name__ == '__main__':
    try:
        pn532 = PN532_SPI(debug=False, reset=20, cs=4)

        ic, ver, rev, support = pn532.get_firmware_version()
        print('Found PN532 with firmware version: {0}.{1}'.format(ver, rev))

        # Configure PN532 to communicate with MiFare cards
        pn532.SAM_configuration()

        print('Waiting for RFID/NFC card...')
        while True:
            uid = pn532.read_passive_target(timeout=0.5)
            if uid is None:
                continue
            intuid = [x for x in uid]
            print('Found card with UID:', [int(i) for i in uid])
            result = get_file(intuid)
            p = subprocess.Popen(['vlc', '-vvv', '--fullscreen', result])
            while True:
                newuid = pn532.read_passive_target(timeout=0.5)
                if newuid is None or newuid == uid:
                    continue
                p.kill()
                break
    except Exception as e:
        print(e)
    finally:
        GPIO.cleanup()