This is a submission for the MLH x DEV Writing Challenge
What I Built
Reels trained my thumb to leave. A clip has a few seconds before I decide it owes me a different clip. I missed the other feeling: a television that is already on, in the middle of something, with nothing under the glass except the picture.
1988.in is that set. You press the glass to turn it on and land in the middle of whatever is on air. There is no progress bar and no next clip waiting. If you want a different picture, you change the channel.
The lineup opens the way an evening in India used to. Channel 001 is Bollywood. Then cricket, Jungle Book, Shaktimaan, a month of DD Classics, Ramayan, and Mahabharat. After those come animals, street food, space, sport, and travel, and then the quieter stations: rain, a hearth, a forest, a lake. Channel 013, Vintage India, is a curated run of old advertisements, public-service films, and broadcast interludes — 198 videos, kept short on purpose, about two hours and fifty minutes from start to finish. The full set is 125 channels. Each one ships as its own list of YouTube video ids, titles, and durations.
1988 is the year on the glass. The programmes come from many years, and every one of them plays through YouTube. The app never downloads, rehosts, or proxies the video.
The piece that makes it feel like a broadcast is a clock. The same channel, the same catalog, and the same UTC second always resolve to the same video and the same start time. Two people who turn the set on in different rooms are watching the same programme, already underway. A catalog shorter than a day rotates its opening programme by the UTC date, so Tuesday is a different timetable from Monday. A catalog longer than a day plays straight through midnight. When a video ends, the clock chooses the next slot. YouTube's up-next row never gets a vote.
The YouTube player is real, and its controls are hidden. Scanlines, grain, and a vignette are a CSS layer on top of the iframe. A CSS filter on the iframe itself fights the player, so the glass treatment stays on a separate layer and ignores pointer events. Changing channel runs a short analog zap: snow, a horizontal tear, and the new picture already loading underneath. prefers-reduced-motion keeps still scanlines and replaces the zap with a brief dark frame. Volume lives on the glass, in steps of five. The channel and volume HUD fades after a few seconds of quiet. The first tap, arrow, or OK press only wakes it.
A phone can be the remote. The computer shows a QR code and an eight-character code. The phone joins over a WebSocket and can send power, channel, volume, and mute. The desktop answers with its real state, including changes made on the glass. Picture and sound stay on the computer. One phone pairs with each set. An invite lasts five minutes and is used up on the first join. A disconnected desktop has two minutes to return. A session lasts twelve hours. Presses made while the phone is offline are dropped. A remote that replays a stale command after it reconnects is a worse remote than one that missed a press.
The app is Vue 3, Vite, Pinia, and Vue Router. One Pinia store owns power, channel, volume, interruption, and the current slot. Components draw the set. They do not call YouTube.
A shared clock is the product. Computing { videoId, startSeconds } from UTC is what makes two browsers agree. A random pick per session would be a website with a bezel.
Search quota ends a demo. YouTube's Data API allows very few search.list calls per day. videos.list is cheap by comparison. Viewers no longer search. Each channel is a JSON file, lazy-loaded only when that station is tuned. The home page, the guide, and the phone never download those lists. Playback works with no API key. When a key is present, it only re-checks that the bundled ids are still embeddable and still have a duration.
Embeddable is a moment, not a promise. Public playlists contain private, deleted, regional, and non-embeddable videos. A catalog that played on Saturday can fail on Sunday. That channel shows an interruption card and retries. Another station is never quietly substituted. If Cricket cannot play, the set does not put on Nature and hope you will not notice.
The power button is the browser gesture and the legal gate. Sound will not start without a click. That same click is where the viewer agrees to the privacy policy, the terms, and YouTube's terms. The Data API and the iframe stay idle until then.
Nostalgia needs a boundary. I wanted the feeling of that era of Indian television. I did not put an official broadcaster mark on the glass. The site says so in the footer.
Demo
The set is live at https://1988.in/.
- Choose Watch TV and turn the set on. You should arrive in the middle of a programme, with the YouTube controls gone.
- Press CH + or the right arrow. Snow, then a different station, also already in progress.
- Open Channels. Search "cricket", or filter by the DD Era tag, and tune in.
- On the computer, choose Connect remote. Scan the QR code, or open
/remoteand type the eight-character code. Change the channel from the phone. The desktop moves. The phone never shows the video.
Source: github.com/tusharv/positive-player
Partner Technologies
The two platforms this project actually depends on, beyond the Vue app, are the YouTube Data API through Google Cloud, and Vercel.
YouTube Data API on Google Cloud
Playback is the IFrame Player API. That part spends no Data API quota. Catalogs are the expensive problem.
The first lineup was search queries, one per channel. Flipping through the set could burn the daily Search Queries budget, which is a separate, much smaller bucket than the general 10,000-unit pool. I moved every station to a curated bundle. A generator script runs only when I ask it to, never as part of npm run build. It checks metadata, keeps up to 50 videos for a generated channel, preserves hand-picked selections, and refuses to replace the files if any generated channel would be left with fewer than three videos. A changed bundle gets a new cache key, so a viewer's browser does not keep yesterday's list under today's name.
Two stations are hand-kept. Channel 005, DD Classics, is a 34-day rotation of serials, comedy, culture, and vintage ads. Channel 013, Vintage India, keeps the short ad spots that a "drop anything under 60 seconds" rule would have thrown away. Curated items shorter than one second are still dropped. Ordinary catalogs still drop anything under a minute.
Vercel
The television is a static build. The remote needs a public WebSocket, and a static host does not provide one.
vercel.json rewrites /remote-ws to a Function, api/remote-ws.mjs, with maxDuration set to 300 seconds. The function includes the server code, the shared protocol module, and the channel data. The hub checks channel numbers against the real lineup, so a phone cannot tune to a station that does not exist.
The function holds an in-memory session hub. The desktop creates a session and receives an id, a host token, the eight-character code, and an invite. The phone joins with the code or the invite and receives a remote token. From then on the phone sends commands (channelStep, volumeStep, digit, mute, powerOff, powerToggle) and the desktop sends state. The host is authoritative. The phone cannot write the channel except by a command the TV applies through the same store as a key on the glass. The socket's origin has to match the request host. Idle connections are swept.
The ceiling is honest. Sessions live in that one process, up to 1,000 televisions, and they disappear when the process does. Two instances cannot share one paired phone and TV. For a room of sets, that is the right shape. A remote that has to survive many instances needs shared session storage and a way to deliver a command to the process that still holds the desktop socket. Video never passes through the function. The bytes go from YouTube to the browser.
Locally, the same hub runs in Node and Vite proxies /remote-ws to it. One protocol module, two places it can run. A self-hosted box can also serve the built site and the socket from a single Node process.
Hackathon Experience
The moment from the software that I keep is a small one. Two browsers, one channel, the same second of the same video, with no server choosing the schedule. That was the first time it felt like a television.
Top comments (0)