DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

Auto-pause YouTube Videos with Stimulus

This article was originally published on Rails Designer


I recently published an article to auto-pause a video using Stimulus when it is outside of the viewport which I built for one of my Rails UI Consultancy clients. In this article I am exploring the same feature but using an embedded YouTube player using an iframe. While the implementation uses the same core concept as the previous video controller (the Intersection Observer API), working with YouTube's iframe API adds some interesting complexity.

If you want to check out the full setup, check out this repo.

Again, let's start with the HTML:

<div data-controller="youtube" data-youtube-percentage-visible-value="20">
  <iframe
    data-youtube-target="player"
    src="https://www.youtube.com/embed/dQw4w9WgXcQ?enablejsapi=1"
    frameborder="0"
    allowfullscreen
  ></iframe>
</div>
Enter fullscreen mode Exit fullscreen mode

Note a few important details:

  • add enablejsapi=1 to the iframe URL to allow JavaScript control;
  • wrap the iframe in a div with the controller (since you'll need to observe the container);
  • target the iframe with data-youtube-target="player".

Now let's generate the Stimulus controller: bin/rails generate stimulus youtube.

Let's break down the controller piece by piece:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["player"]
  static values = {
    playing: { type: Boolean, default: false },
    percentageVisible: { type: Number, default: 20 } // percentage of video that must be visible (0-100)
  }
Enter fullscreen mode Exit fullscreen mode

The controller needs a target for the iframe and two values (the same as the previous controller):

  • player: target for the YouTube iframe;
  • playing: tracks whether the video was playing when it left the viewport;
  • percentageVisible: how much of the video must be visible to be considered "in view" (defaults to 20%).
  connect() {
    this.#setup()
  }

  disconnect() {
    this.observer?.disconnect()
  }
Enter fullscreen mode Exit fullscreen mode

When the controller connects, it calls the private #setup() method to initialize the YouTube API. When disconnecting, it cleans up the observer with optional chaining to avoid errors if the observer wasn't created.

  #setup() {
    if (window.YT) {
      this.#createPlayer()

      return
    }

    window.onYouTubeIframeAPIReady = () => this.#createPlayer()
    const tag = document.createElement("script")
    tag.src = "https://www.youtube.com/iframe_api"

    document.head.appendChild(tag)
  }
Enter fullscreen mode Exit fullscreen mode

The #setup() method handles loading the YouTube iframe API. If the API is already loaded (window.YT exists), it creates the player immediately. Otherwise, it loads the API script and sets up a callback to create the player once the API is ready. With this approach you don't load the YouTube API multiple times if you have several videos on the page.

  #createPlayer() {
    if (!this.playerTarget) return

    this.player = new YT.Player(this.playerTarget, {
      events: {
        "onReady": () => this.#detectViewport(),
        "onStateChange": (event) => {
          if (event.data !== YT.PlayerState.PLAYING) return

          this.playingValue = false
        }
      }
    })
  }
Enter fullscreen mode Exit fullscreen mode

The #createPlayer() method initializes the YouTube player with the iframe API. It sets up two event handlers:

  • onReady: calls #detectViewport() once the player is ready;
  • onStateChange: resets playingValue to false when the video starts playing (to avoid auto-resuming if the user manually paused).
  #detectViewport() {
    this.observer = new IntersectionObserver(
      ([entry]) => this.#adjustPlayback(entry),
      { threshold: this.#thresholdValue }
    )

    this.observer.observe(this.element)
  }
Enter fullscreen mode Exit fullscreen mode

Just like in the video controller, #detectViewport() creates an IntersectionObserver to watch when the video enters or exits the viewport. The threshold comes from the percentageVisibleValue converted to a decimal.

  #adjustPlayback(entry) {
    if (!entry) return
    if (!entry.isIntersecting) {
      this.#pauseWhenOutOfView()

      return
    }

    this.#resumeIfPreviouslyPlaying()
  }
Enter fullscreen mode Exit fullscreen mode

When visibility changes, #adjustPlayback() is called. If the video is not visible, it pauses. If visible, it potentially resumes playback. The early returns create a clean flow without nested if/else statements.

  #pauseWhenOutOfView() {
    if (!this.player || this.player.getPlayerState() !== YT.PlayerState.PLAYING) return

    this.player.pauseVideo()

    this.playingValue = true
  }
Enter fullscreen mode Exit fullscreen mode

The #pauseWhenOutOfView() method first checks if the player exists and if the video is currently playing. If it is, it pauses the video and sets playingValue to true to remember it was playing when it left the viewport.

  #resumeIfPreviouslyPlaying() {

    if (!this.playingValue) return

    this.#attemptToPlay()
  }
Enter fullscreen mode Exit fullscreen mode

If the video returns to the viewport, #resumeIfPreviouslyPlaying() checks if it was playing before. If it was, it attempts to resume playback.

  #attemptToPlay() {
    if (!this.player) return

    this.player.playVideo()

    this.playingValue = false
  }
Enter fullscreen mode Exit fullscreen mode

The #attemptToPlay() method plays the video if the player exists and resets the playingValue to false. Unlike the HTML5 video API, YouTube's API doesn't return a Promise from playVideo(), so you don't need the Promise handling from the previous controller.

  get #thresholdValue() {
    return this.percentageVisibleValue / 100
  }
}
Enter fullscreen mode Exit fullscreen mode

This getter converts the percentage (like 20%) to a decimal (0.2) for the IntersectionObserver API.

And that's it! A clean, focused controller that pauses YouTube videos when they're scrolled out of view and resumes them when they return to view.

The controller handles the complexity of the YouTube iframe API while maintaining the same core functionality as the native video controller.

Want to learn more about writing modern and readable JavaScript code for Rails apps? Check out JavaScript for Rails Developers where all these techniques are covered in depth. ๐Ÿ˜Š

Top comments (0)