Picture-in-Picture with Hotwire in Ruby on Rails

Let’s imagine we have a simple app called Turbo Video, where users can watch their favorite videos. The app is straightforward — it displays the video’s title, description, and a video player.

Something like this:

The code for this view might be something like this:

<!-- videos/show.html.erb -->

<h1><%= @video.title %></h1>

<%= video_tag @video.src %>
<p> <%= @video.description %> </p>

Now we’re looking to improve the UX so users can keep watching their current video while browsing for similar ones.

When “See more videos like this” button is clicked, a list of similar videos should appear, while the player continues playing the video from the previous screen:

Keep playing

I’m not going to cover how to build the list of videos, since that’s just a matter of querying the database with certain criteria and displaying the results. Instead, let’s focus on how to keep the player running even after navigating to another page.

To do this, we could use the data-turbo-permanent attribute, which tells Turbo to persist the element across page loads.

According to documentation: Before each render, Turbo Drive matches all permanent elements by ID and transfers them from the original page to the new one, preserving their data and event listeners.

So first, let’s add an ID to the video player and include the same player on the similar videos page.

<!-- videos/show.html.erb -->

...
<!-- 
  We add the id and data-turbo-permanent attribute to 
  the existing video player 
-->
<%= video_tag @video.src, id: "player", data: { permanent: true } %>
...
<!-- videos/similar.html.erb -->

...
<!-- We add the video tag with same id and data-turbo-permanent attribute -->
<%= video_tag id: "player", data: { permanent: true } %>
...

We don’t even need the src attribute in the video_tag on the similar videos page because, thanks to data-turbo-permanent, the element won’t be reloaded.

With this, the player will keep playing the video seamlessly 😎.

Picture in picture

As I showed in the sketch, we want the video to appear in the bottom-right corner. We can achieve this by adding a CSS class to the video player when listing similar videos. Let’s build a Stimulus controller for that.

The player will be a target in our Stimulus controller, and the CSS class will be added or removed depending on the page we’re on.

// controllers/video_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["player"]
  static values = { pip: Boolean }

  playerTargetConnected() {
    // When the player target connects, 
    // it sets a CSS class based on the pipValue
    this.playerTarget.classList.toggle("picture-in-picture", this.pipValue)
  }
}

Now, let’s update our views to connect the Stimulus controller

<!-- videos/show.html.erb -->

<!-- 
  We wrap everything in a <div> connected to a Stimulus controller. 
  pipValue should be false because we want the full version of the
  player
-->
<div data-controller="video" data-video-pip-value="false">
  ...
  <%= video_tag @video.src, id: "player", 
    data: { permanent: true, video_target: "player" } %>
  ...
</div>

We’ll use the same stimulus controller but the “pip” param value should be true:

<!-- videos/similar.html.erb -->

<!-- The data-video-pip-value is true -->
<div data-controller="video" data-video-pip-value="true">
  ...
  <%= video_tag id: "player", data: { permanent: true, video_target: "player" } %>
  ...
</div>

With that we have our picture in picture mode 🤩.


If you’re interested in learning more about Hotwire, I invite you to read the following articles: