Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/zshall/program-guide/llms.txt

Use this file to discover all available pages before exploring further.

Every watchable channel in Television Simulator ‘99 plays a background video sourced from YouTube. This approach lets the simulator reuse publicly available archival footage — such as the 90s commercial compilation on Channel 12 — without bundling large video files. The YouTube IFrame Player API manages playback; a thin YouTubeApi helper class wraps its methods; and the TV class ties mute state to the on-screen overlay so the viewer always knows when audio is silenced.

Loading Sequence

The YouTube IFrame API must be ready before the guide can load, because the default channel’s player is instantiated as soon as the channel class is created. The loading chain is:
1

TV.startUp() calls YouTubeApi.loadYouTubeAPI()

loadYouTubeAPI() is an async method that calls Helpers.loadScript with the IFrame API URL. Helpers.loadScript creates a <script> tag, appends it to <head>, and returns a Promise that resolves on script.onload.
static async loadYouTubeAPI() {
    await Helpers.loadScript("https://www.youtube.com/iframe_api");
}
2

YouTube fires onYouTubeIframeAPIReady

Once the injected script downloads and initialises, YouTube’s API calls the globally defined onYouTubeIframeAPIReady function. The implementation in youtube-ctrl.js dispatches a custom DOM event instead of coupling directly to the TV class:
function onYouTubeIframeAPIReady() {
    document.dispatchEvent(new CustomEvent('youtubeReady', {}));
}
3

TV listens for the youtubeReady event

Inside TV.startUp(), a document event listener waiting on youtubeReady fires. It performs the AJAX fetch of guide.xml, parses the response, and calls showChannel for the default channel — guaranteeing the YT global is available before any channel tries to instantiate a player.
document.addEventListener('youtubeReady', () => {
    $.ajax({
        type: 'GET',
        url: 'data/guide.xml',
        dataType: 'xml',
        complete: (data, status) => {
            window.guideData = $($.parseXML(data.responseText));
            var defaultChannel = guideData.find('channel[watchable][default]').attr('number');
            if (undefined != defaultChannel && null != defaultChannel) {
                this.showChannel(defaultChannel);
            }
        }
    });
});

YT.Player Instantiation in Channel12

Inside Channel12.show(), a YT.Player is created targeting the #ytplayer div that was injected by layout.html. The video starts automatically, native controls are hidden, and related-video suggestions are suppressed:
this.player = new YT.Player('ytplayer', {
    height: '360',
    width:  '425',
    videoId: '9NSVU4Gv_wA',
    events: {
        'onReady':       this.onPlayerReady,
        'onStateChange': this.onPlayerStateChange
    },
    playerVars: {
        'autoplay': 1,
        'controls': 0,
        'rel':      0
    }
});
onPlayerReady calls event.target.playVideo() to ensure playback begins even if autoplay is blocked by the browser. onPlayerStateChange logs when the video enters the playing state and can be extended to handle end-of-video looping.
controls: 0 removes the native YouTube player bar entirely. Viewers cannot pause, seek, or mute using YouTube’s own UI. Mute must be performed through the TV’s hardware mute button, which routes through TV.toggleMute() and YouTubeApi.
The player instance must be assigned to this.player on the channel object. TV.toggleMute() accesses this.channel.player directly — if player is not exposed, mute operations will silently fail.

YouTubeApi Static Methods

The YouTubeApi class in youtube-ctrl.js is a thin wrapper around the YT.Player instance API. All methods are static and accept a player reference as their first argument.

loadYouTubeAPI()

Async. Injects the https://www.youtube.com/iframe_api script tag using Helpers.loadScript. Returns a Promise that resolves when the script finishes loading.
static async loadYouTubeAPI() {
    await Helpers.loadScript("https://www.youtube.com/iframe_api");
}

restartVideo(player)

Stops the current video then immediately calls playVideo(). Useful for looping back to the beginning of a clip.
static restartVideo(player) {
    YouTubeApi.stopVideo(player);
    player.playVideo();
}

stopVideo(player)

Calls player.stopVideo(), which halts playback and resets the playhead to the beginning of the video.
static stopVideo(player) {
    player.stopVideo();
}

toggleMuteVideo(player)

Checks the current mute state with player.isMuted(). If muted, calls player.unMute(); otherwise calls player.mute(). Returns !player.isMuted() evaluated immediately after the mute/unmute call. TV.toggleMute() stores this value as this.muted and shows the MUTING overlay when it is true.
static toggleMuteVideo(player) {
    if (player.isMuted()) player.unMute();
    else player.mute();
    return !player.isMuted();
}

muteVideo(player)

Unconditionally mutes the player.
static muteVideo(player) {
    player.mute();
}

unmuteVideo(player)

Unconditionally unmutes the player.
static unmuteVideo(player) {
    player.unMute();
}

Mute State Integration

TV.toggleMute() calls YouTubeApi.toggleMuteVideo with the current channel’s player, stores the resulting boolean, and then updates the on-screen overlay via afterMute():
toggleMute() {
    this.muted = YouTubeApi.toggleMuteVideo(this.channel.player);
    this.afterMute();
}

afterMute() {
    if (this.muted) {
        $('#tvm-bottom-left').text('MUTING');
    } else {
        $('#tvm-bottom-left').text('');
    }
}
When this.muted is true, the text MUTING appears in the #tvm-bottom-left div overlaid on the TV screen (bottom-left corner). When the viewer unmutes, the text is cleared. The #tv-mute element in index.html is an invisible clickable region styled to overlap the physical mute button area of the TV image:
<a id="tv-mute">&nbsp;</a>
Its click handler is wired in TV.startUp():
$('#tv-mute').click(() => {
    this.toggleMute();
});

Player Reference Pattern

When TV.showChannel switches channels it calls teardown() on the old channel. The base Channel.teardown() checks for this.player and calls YouTubeApi.stopVideo if it exists:
teardown() {
    // …clear timers…
    if (this.player) {
        YouTubeApi.stopVideo(this.player);
    }
    this.container.empty();
    this.container.attr('id', '');
}
This means any channel that creates a YT.Player must store it as this.player to get automatic cleanup — and to allow TV’s mute controls to reach it. A channel that stores its player under any other property name (e.g. this.ytPlayer) will not be stopped on channel change and will not respond to mute.
If a future channel needs multiple simultaneous YouTube players, store the primary playback player as this.player for mute-control compatibility, and manage additional players manually in your teardown() override.

Build docs developers (and LLMs) love