Selling digital products using Laravel part 6: Building a video section using Vimeo
On our website, we have a video section. All our videos uploaded to and handled via Vimeo where we have a Pro subscription. We chose Vimeo because it has an excellent widget to display videos, it converts our videos very fast, and it has a nice API to work with. Let's take a look at how Vimeo is integrated in our site.
- Part 6: Building a video section using Vimeo (you are here)
Videos on our site are grouped per series, such as "Laravel Beyond CRUD", "Laravel Package Training", ... When clicking on a series, you'll see the outline of that series. Some of the videos can be viewed for free. Other videos require you to have purchased the product the series belongs to. Other videos can be viewed when sponsoring us. Let's take a look at how we built this.
All our videos are uploaded on Vimeo where we have a Pro subscription. We chose Vimeo because it has an excellent widget to display videos, it converts our videos very fast, and it has a nice API to work with.
At Vimeo, for each uploaded video, we input a title and a description. In the description field, we use markdown. On spatie.be, that markdown will be rendered as HTML. For each video, we specify that only people with the private link can view it. This will have the videos have an unguessable URL. At spatie.be we'll retrieve the right URL using the API.
Adding videos using Nova
Let's take a look at how we integrate spatie.be with Vimeo. At spatie.be we built a Laravel Nova powered admin panel. In the video section, we can add a new video by specifying the Vimeo video id. We can also specify who can view the video.
Let's take a look at how it works under the hood. In the booted
method you'll see this line that will execute UpdateVideoDetailsAction
each time a Video
model gets saved.
static::saved(fn (Video $video) => app(UpdateVideoDetailsAction::class)->execute($video));
This is the entire UpdateVideoDetailsAction
class.
class UpdateVideoDetailsAction
{
private Vimeo $vimeo;
public function __construct(Vimeo $vimeo)
{
$this->vimeo = $vimeo;
}
public function execute(Video $video): Video
{
$video->withoutEvents(function () use ($video) {
$vimeoVideo = $this->vimeo->getVideo($video->vimeo_id);
$slug = Str::slug($vimeoVideo['name']);
$video->update([
'slug' => $slug,
'title' => $vimeoVideo['name'],
'description' => $vimeoVideo['description'],
'runtime' => $vimeoVideo['duration'],
'thumbnail' => $vimeoVideo['pictures']['sizes'][1]['link'],
]);
});
return $video;
}
}
In that class above, we use the Vimeo
class to fetch and update our local Video
model with the title, runtime, thumbnail, and more. This code is wrapped in withoutEvents
. This is necessary because otherwise, the update inside this class would trigger UpdateVideoDetailsAction
again, and we'd get stuck in an infinite loop.
That Vimeo
class above is a plain wrapper around the Vimeo API.
namespace App\Services\Vimeo;
use GuzzleHttp\Client;
class Vimeo
{
private Client $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function getVideos(): array
{
$response = $this->client->get('https://api.vimeo.com/me/videos', [
'query' => [
'per_page' => 100,
],
]);
$data = json_decode($response->getBody()->getContents(), true);
return $data['data'];
}
public function getVideo(string $vimeo_id): array
{
$response = $this->client->get("https://api.vimeo.com/videos/{$vimeo_id}");
return json_decode($response->getBody()->getContents(), true);
}
}
Displaying videos
In the videos/show
blade view, a video is displayed. Here is the part where the Vimeo player is rendered.
@if ($currentVideo->canBeSeenByCurrentUser())
<iframe id="player" class="absolute inset-0 w-full h-full"
src="https://player.vimeo.com/video/{{ $currentVideo->vimeo_id }}?loop=false&byline=false&portrait=false&title=false&speed=true&transparent=0&gesture=media"
allowfullscreen allowtransparency></iframe>
@else
We just use the standard Vimeo player embed that gets passed the vimeo_id
of the video that should be displayed.
On the Video
model, we store which audience is allowed to see the video in the display
attribute. The model has canBeSeenByCurrentUser
method that determines if the video can be seen by the currently logged in user.
public function canBeSeenByCurrentUser(): bool
{
if ($this->display === VideoDisplayEnum::FREE) {
return true;
}
if (! auth()->check()) {
return false;
}
if ($this->display === VideoDisplayEnum::AUTH) {
return true;
}
$userOwnsSeries = $this->series->isOwnedByCurrentUser();
if ($this->display === VideoDisplayEnum::SPONSORS) {
return auth()->user()->isSponsoring() || $userOwnsSeries;
}
if ($this->display === VideoDisplayEnum::LICENSE) {
return $userOwnsSeries;
}
return false;
}
If this function does not allow a user to see a video, we display a friendly message informing the user what needs to be done to be able to see the video.
Marking a video as completed
Each video can be marked as completed. In this video, I explain how that works under the hood. How meta!
This series is continued in part 7: Importing package documentation from GitHub.
What are your thoughts on "Selling digital products using Laravel part 6: Building a video section using Vimeo"?