ENGLISH·COURSE API
    Preparing search index...
    • The lesson player: a Vidstack <MediaPlayer> with the Default Video Layout.

      Parameters

      • source: {
            source: string;
            poster?: string;
            title: string;
            ariaLabel?: string;
            keyDisabled?: boolean;
            children?: ReactNode;
            ref?: Ref<MediaPlayerInstance | null>;
        } & Pick<
            Omit<MediaPlayerProps, "ref"> & RefAttributes<MediaPlayerInstance>,

                | "onEnded"
                | "onPause"
                | "onPlay"
                | "onPlaying"
                | "onSeeking"
                | "onTimeUpdate",
        >

        The lesson's video URL: a project-hosted file, or a YouTube link in any of its forms

      Returns Element

      It replaces the bare <video controls> this project shipped in v1. The reason is not the chrome — it is that a native player's controls live in a closed shadow root, so anything drawn over them is at the browser's mercy in both z-order and hit-testing. Here the provider, the layout, and any overlay are siblings in a tree we own.

      The overlay is a children slot, not a prop, precisely so it composes that way: it lands inside the player element, sharing its positioning context, and the caller styles it against the player box without this component knowing what the overlay is or when it shows.

      ariaLabel is passed through rather than left to Vidstack. The library otherwise builds "Video Player - <title>" in English, which would be the one control name on the page that no locale file can reach.

      The src is derived from source, not passed through. A lesson's video is either a file this project hosts or a video published on YouTube, and the two need different providers. youtubeVideoIdFrom decides which; a YouTube link becomes Vidstack's youtube/<id> provider form, everything else stays a direct video/mp4 source. Declaring type: "video/mp4" for a YouTube link — which is what every lesson used to get — makes the provider read a watch page as a byte stream and the lesson shows a dead player.

      Routing YouTube through the library's provider rather than a hand-written <iframe> is what keeps the rest of this file true for both kinds of lesson. The provider drives a YouTube iframe through its IFrame API and exposes it behind the same MediaPlayerInstance, so currentTime, seekTo and the media events keep working — which is what the resume overlay and the position persistence are built on. A bare embed would strand both: its start= parameter can say where to begin, but nothing can read back where the learner stopped.

      Fullscreen stays the browser's wherever the browser has it. The layout's own fullscreen button is left in place and a fallback is added after it, so a browser that can take the player fullscreen behaves exactly as it did before this control existed. The fallback exists for the case the library cannot serve: Safari on iPhone exposes no element Fullscreen API, a YouTube-sourced lesson has no <video> for webkitEnterFullscreen, and the library hides a button it cannot support, which left an iPhone learner with no way to enlarge a lesson at all. Exactly one of the two ever paints — VideoEnlargeButton renders nothing while canFullscreen is true.

      Enlarged, the player is pinned to the viewport as the largest 16:9 box that fits, over a black backdrop, rather than stretched to the viewport's own shape. That is forced by the YouTube provider: the embed lays its video out against the iframe's width and the player shows only the middle band, so a band shorter than width × 9/16 — which is what a landscape viewport would give — crops the video top and bottom.

      The browser's own fullscreen has the same trap and gets the same answer one level down. There the browser makes the player the screen, so on a screen wider than 16:9 — an Android phone in landscape — the stylesheet bounds the provider instead: the video is the largest 16:9 band, centred, while the controls and gestures keep the whole screen.

      On an iPhone the viewport itself is the ceiling: Safari's toolbar takes 110 of the 402 landscape points and hides only for a real swipe on the document, which is why the mode never locks the page's scroll — the swipe has to travel through the pinned player to the page beneath. While that toolbar is still on screen a ScrollDownHint is drawn along the top of the box, asking the learner to move the video upward — the gesture, not the page's answering scroll, because the backdrop hides the page. useBrowserChromeVisible decides when, from what the page can measure, and the hint is gone the moment the viewport reaches the screen's short side.

      A single tap on the video means what the pointer's convention says. The Default Layout's own gestures are switched off (noGestures) and PlaybackGestures supplies the set, because the layout picks a tap's meaning with a media query in its stylesheet and this Player needs its own seek run and speed hold beside it. With a mouse a click toggles playback; with a finger a tap brings the control bar in or out, as every video app on a phone does, and only a play/pause control pauses.

      That leaves one trap to answer. Safari on iPhone gets YouTube's mobile skin, which draws a centred play/pause icon through this chrome even with the embed's controls disabled; the provider's blocker keeps every tap from reaching it (rightly — the same overlay carries links out of the lesson); and the icon cannot be hidden from outside a cross-origin frame. The compact chrome covers it with a centre button of its own, but the full chrome — the one this Player wears enlarged in landscape — has none, so VideoCenterPlayButton draws one over the icon while the controls are in view. A double tap on an edge starts a seek run with an on-screen count, the YouTube app's convention — the helper's own JSDoc says how the library's gesture and the run share it.

      A press held on the video runs the lesson at double speed, that app's other thumb convention, and restores the learner's own rate when the finger lifts. The play/pause key carries the same gesture — useSpeedHold says how it is taken from the library, which acts on that key's keydown and would otherwise pause the lesson half a second before a hold could arm. The library has no hold event, so the press is timed against the player element; PlaybackGestures and useSpeedHold carry the reasoning, including why a press that drifts is treated as the swipe that hides Safari's toolbar rather than as a hold.

      How far that seek reaches is the learner's to set, from SeekStepMenu in the layout's settingsMenuItemsEnd slot. It rides the library's gear menu rather than a control of this app's own precisely because that menu is already placed, keyboard-navigable and touch-sized in both layouts and inside the pinned player; the component's own JSDoc has the rest.

      The element is never portalled. Moving the player in the tree would remount the provider <iframe>, reloading the embed and resetting currentTime under the resume overlay and the position writes; a class costs none of that.

      The buffering indicator is the Player's own, in the layout's bufferingIndicator slot. The Default Layout's ring is hollow, and a YouTube-sourced lesson's embed paints its own spinner inside it at the very same moments — the player's waiting is derived from the embed's Buffering state — so VideoBufferingIndicator draws the same ring over an opaque core that hides it. For the same reason the centre play/pause control, in both chromes, is an opaque disc larger than the icon the embed paints under it; lesson-video-player.css holds that geometry once.

      Four of the player's defaults are wrong for this app and are overridden here rather than worked around by callers:

      • The poster is drawn explicitly, for every lesson. The Default Layout renders no Poster of its own, so a self-hosted lesson with a perfectly good thumbnail would show a black idle frame. It belongs inside <MediaProvider>, which is the outlet the provider paints behind the video. It is rendered whether or not the lesson has a poster, because the element resolves its own picture — the lesson's, else the thumbnail the provider discovers — and hides itself when there is neither. For a YouTube lesson that is not a nicety: the embed paints a red play button over its thumbnail until the first play, and this element, painted over the frame until frames roll, is the one thing that hides it.
      • The color scheme follows the app, not the OS. Vidstack defaults to system; this app has its own toggle that ignores the OS, so the chrome would sit in light mode inside a dark page.
      • The layout breaks on width alone. The default also switches to the compact mobile layout below 380px of height, and a 16:9 player in the lesson column is ~360px tall on a desktop — every desktop learner would get the phone chrome. isCompactChrome holds the threshold, so the centre play control asks the same question the layout does.

      Captions, chapters, thumbnails, quality menus, and Cast are all available from this layout and none are wired up here; adding one is a change to this file, not a change of player.