Collecting Time Video Metrics with Livewire

A monitor stands on top of a table. The monitor contains a video player that is paused, displaying the play button in the middle. Next to the monitor is a hanging clock.
Image by Annie Ruygt

Today weโ€™ll use Livewire to communicate video event data to our server. Deploy now on Fly.io, and get your Laravel app running in a jiffy!

Alright drumrolls. Because today, we’ll intercept three user-video interaction events, and use Livewire to easily send event data to our server!

The Problem

There are cases when we’d want to get insight on how users interact with the video files we display in our website pages.

For example. Let’s imagine we have a Laravel website where we share online courses. Each course would have video-lessons users can watch and complete in order to progress through a course.

Wouldn’t it be neat to know which specific time users usually pause lessons in? Or how long does each pause take before the user plays the video again? How about points in time that are frequently visited by the users?

Solution

There are several events a video tag emits which we can hook onto to gain insight on how users interact with various video-lessons. In this article, we’ll demonstrate monitoring user-video interaction data through three events:

  1. pause event - to determine the point in time users usually pauses a lesson
  2. play event - to determine how long pauses endure before lessons are played again
  3. seeked event - to determine the point in time that is usually jumped into for a lesson

Today, we’ll hook onto these events to get relevant data, and use Livewire to easily share data to our server ๐ŸŒŸ

Version Notice. This article focuses on Livewire v2, details for Livewire v3 would be different though!

Setting Up The Video Route

We’ll be accessing our video file through an endpoint:

Route::get('/videos/{id}',
    [\App\Http\Controllers\VideoController::class,'stream']
)->name('videos.stream');

Our VideoController class will have a simple stream method:

public function stream(Request $request, $id){
    // getFileDetails() is a custom method to get our video file!
    $details = $this->getFileDetails( $id );
    $header = [
        'Cache-Control','no-cache, must-revalidate'
    ];
    return response()->file(storage_path($details['path']),$header);
}

Now that we have a route to our video file, let’s proceed with creating a Livewire component we can play this video file in.

Setting Up the Livewire Component

Let’s create our Livewire component with php artisan make:livewire video-player. This should create two files: A component, and a view. We can embed this component to any blade view, and even pass parameters to it, likeso:

@livewire('video-player', ['videoId'=>1])

To use videoId, we have to declare a matching public attribute in our Component. We’ll use this later below to create the route to access our video file. First, let’s revise our view.

We’ll add a video tag with no source yet; Instead of immediately getting the source, we’ll let the page render the video tag first. Then, we’ll use Livewire’s wire:init directive to trigger loading the source after rendering:

<!-- view -->
<div wire:init="initVideo">
  <video wire:ignore
    id="videoPlayer" controls width="500px" height="900px"
  />
</div>

Once the page completes loading, the wire:init directive will call the method initVideo(). We can do some initial processing in this method. An example would be to generate a temporary $url to access our video file.

Then we can use Livewire’s dispatchBrowserEvent() method to notify the view’s JavaScript to receive this url by emitting a custom event called init-complete:

/* Component */
public $videoId;

public function initVideo()

  // As mentioned above, generate url with $videoId
  $url =  URL::temporarySignedRoute(
    'videos.stream', now()->addMinutes(30), ['id' =>  $this->videoId]
  );
  // Notify client to set source
  $this->dispatchBrowserEvent('init-complete', ['url'=>$url]);
}

We can listen to this event in our JavaScript, and finally set the source of our video tag:

<!-- view -->
<script>
  var video = document.getElementById("videoPlayer");
  window.addEventListener('init-complete', event => {
    video.src = event.detail.url;
  });

Now that we have our video tag loading our source( neatly after page load ), it’s time to monitor our user interaction with the video!

Fly.io โค๏ธ Laravel

Fly your servers close to your users—and marvel at the speed of close proximity. Deploy globally on Fly in minutes!

Deploy your Laravel app! โ†’

Only Time Can Tell

One of the main data we can use to get insight on how our user interacts with our video-lessons is through time data. In this article, we’ll use two main sources for insight on time:

  1. A video tag’s currentTime that points to the time in seconds the video is currently at.
  2. A video event’s timestamp that points to the time in milliseconds the event occurred in.

With these two time variables, we can build insight on our user’s time-based interaction with their video lessons:

When do users often pause the Video-Lesson?

Let’s say we want to listen in on which part of our video-lessons users mostly pause on. We can do so by listening to the pause event triggered whenever the user pauses the video, and get the video tag’s currentTime to determine at what video moment the user paused the video in:

Then we can use Livewire to discreetly send this information to be processed in the server through a custom method, @this.sendPauseData().

<!-- view -->
<script>
// ... previous listener here

video.addEventListener('pause',function(e){
    // readyState = 4 ensures true pause interaction
    if( video.readyState == 4 )
      @this.sendPauseData( video.currentTime );
});

@this.sendPauseData() will trigger a request to call a method in our Livewire component named sendPauseData(). We’ll use this method to share the retrieved currentTime in our server, and ultimately record the “pause” event details.

Doing so let’s us keep a history of pauses for our lesson, paving way for determining “possible” engagement-insights on the lesson: like its average paused video moment.

/* Component */
public function sendPauseData( $videoCurrentTime ){
    // Do some processing on $videoCurrentTime
}

How long do users pause at this Video-Lesson moment?

Since we’re recording different points in time a video gets paused in, why not also add in how long the user paused the lesson? Duration would be: (the time played after pause) - (the time paused).

To get the pause time, we simply use timeStamp from the event variable passed in the pause listener event. Then to get our played time, simply get the timeStamp from the play event listener!

Let’s revise our pause event to include the e.timeStamp value. We’ll also create a global variable to tell us the video has been paused—adding this variable will help us later in listening for “true” play events.

<!-- view -->
<script>
// ... previous listener here

+   var isPaused = true;// Our video is initially on pause
video.addEventListener('pause',function(e){
    // readyState=4 ensures true pause event interaction
+    if( video.readyState == 4 ){
+       isPaused = true;
+       @this.sendPauseData( video.currentTime, e.timeStamp );
+    }

Then in our method from the Livewire component, we’ll have to track this timestamp as a public property $pausedAt so it doesn’t get lost:

/* Component */
+ public $pausedAt;

public function sendPauseData( $videoCurrentTime, $pausedAtTimeStamp ){
+    $this->pausedAt = $pausedAtTimeStamp;
     // Do some processing on $videoCurrentTime and $pausedAtTimeStamp 
}

Next, let’s set up our play event listener. When the user finally plays the video in our view, we can simply send the current timeStamp to our Livewire component:

<!-- view -->
<script>
// ... previous listeners here
var isPaused = true;
video.addEventListener('play',function(e){
    // Make sure video is prev paused
    if( isPaused ){
      isPaused = false;
      @this.sendPlayData( e.timeStamp );
    }
});

And use this value to get the time difference from the $pausedAt attribute previously set:

/* Component */
public function sendPlayData( $playedAtTimestamp ){ 
    if( $this->pausedAt ){
        $diff = $playedAtTimestamp - $this->pausedAt;
        // Do some processing on this new found duration, $diff
    }
}

Which point in time Users usually “seek” to?

There are certain parts of a video that users find really helpful to go back to, we can gain insight on this as well!

Again, we can simply use the currentTime to tell us at which lesson moment the user moved or “seeked” the video time to:

<!-- view -->
<script>
// ... previous listeners here

video.addEventListener('seeked',function(e){
    console.log('User went to:', video.currentTime);
    @this.sendSeekedData( video.currentTime );
});

And in our Livewire component, make our due processing with a matching method:

/* Component */
public function sendSeekedData( $videoCurrentTime ){
  // Record this vide time please, with a "seekedTo" type
  // to gather historical data
  // that can be basis for determining popular video timestamps!
}

Catching Time

Time does fly! And often times it’s a bit hard to catch up to it—for example, we just can’t stop time adding up to the years in our lives. Luckily the same isn’t true for time in video analytics and Livewire.

In this article, we’ve learned about three video events: pause, play, and seek. Listening on these events helped us to get data on three “time-centric” ways users interact with our video-lessons through: the video tag’s currentTime and a video-event’s timeStamp value. Finally, we showcased how easily data from video tags in client browsers can be shared to our server with the help of Livewire.

Of course there’s so much more video events we can listen on and test out, if that’s an interest, reading through the HTMLMediaElement documentation would help for other video analytics implementations.

Time does fly, and we can’t often use it wisely. But, using Livewire to share video events from client browser to our server—now that’s one time-wise way to get time analytics sent to our Laravel applications. ๐ŸŒŸ