Ionic 4: Autoplay videos on scroll
Often, achieving Instagram or Facebook type experience is more difficult than it sounds in Ionic. To bridge that gap, I’m going to show you how to automatically play and pause videos in your app as your user scrolls.
This article will be a bit longer, despite the topic being pretty simple. The reason being, is I like to explain WHY we’re doing things, instead of just having you blindly copy and paste, without understanding what’s actually going on in your application.
Let’s get started
First and foremost, we need to edit our config.xml
file to include the following preference.
<preference name=”AllowInlineMediaPlayback” value=”true” />
This tells our app that we want to be able to choose which videos are played inline, without opening the native video player — which can be annoying in an app with a lot of user-generated videos.
Edit Config.xml
Your config.xml
file should end up looking something like this:
...
<preference name="ScrollEnabled" value="false" />
<preference name="android-minSdkVersion" value="19" />
<preference name="BackupWebStorage" value="none" />
<preference name="SplashMaintainAspectRatio" value="true" />
<preference name="FadeSplashScreenDuration" value="300" />
<preference name="SplashShowOnlyFirstTime" value="false" />
<preference name="SplashScreen" value="screen" />
<preference name="SplashScreenDelay" value="3000" />
<preference name=”AllowInlineMediaPlayback” value=”true” />
...
Your values might be a little different, but that’s ok. We just needed to add the bolded text to our preferences section.
Unfortunately, that’s not all we need to do, because if you reload, you’ll notice that nothing happened. The videos are just stuck on the first frame and aren’t playing when they scroll into view 🙁.
Package — ng-in-viewport
to the rescue!
ng-in-viewport
is a nifty little npm package designed to handle all of the setup required to determine if an element (our video) is currently in the viewport or not via the intersection-observer api.
While we could do this setup on our own, i’m of the mentality that we shouldn’t be re-inventing the wheel when there is a vetted & tested package that can do what we want to accomplish; and do it better.
You’ll need to install the package via npm install ng-in-viewport
.
Let’s generate some code!
Another lesson I try to always follow (though I’m not perfect) is generating components and classes when something is going to be used in multiple places in my app. It simplifies logic, cuts down on file sizes and allows us to quickly and easily modify our app.
Use the command ionic g component components/inline-video
to generate a new component called inline-video
, which we’ll use to quickly and easily display inline-videos in our Ionic application.
Next, we’re going to use the command ionic g class classes/video
to create a new class called video
. This is going to help us bundle all of our video metadata information into one central location, that we can quickly and easily use later on.
Imports — App.module.ts
We need to make sure that the ng-in-viewport
package is properly imported into our app.module.ts
file. You’ll notice that our inline-video
component has already been imported via the CLI, which is nice quality of life feature to have in V4.
import { InViewportDirective } from 'ng-in-viewport';
...@NgModule({
declarations: [
InViewportDirective,
...
],
...
exports: [
...
InViewportDirective,
],
Lets get down to business…to defeat…the videos!
Who doesn’t love a good Mulan reference? Okay. Back to coding…
Class — video.ts
This is what our video class is going to look like. In this simple example, we are exposing two properties:
src
— which is our video file source
id
— which is a unique id for our video. I’m generating this via Firebase and passing it through the constructor, but you can use another method. I’ll explain why this is a good idea to do, in a little bit.
export interface VideoInterface {
src?: string;
id: string;
}
export class Video implements VideoInterface {
public src: string;
public id: string;
constructor(id: string) {
this.id = id;
this.src = 'https://ia800501.us.archive.org/10/items/BigBuckBunny_310/big_buck_bunny_640_512kb.mp4';
}
}
I’ve included a free to use video for testing purposes as the video source. This means that all of our videos will be playing the same thing. This is good enough for testing purposes.
Component — inline-video.component.ts
We’re going to import some information from our new package & class into the inline-video.component.ts
file, same as we would anything else.
import { InViewportMetadata } from 'ng-in-viewport';
import { Video } from '../../../classes/video';
Even though the package is technically a directive, we’ll need the InViewportMetadata
information to properly interact with our videos.
We’ll also want to create a video object to use in the component, using our new video
class we implemented earlier.
public video: Video = new Video('someUniqueId');
You’ll also notice that i’m passing a unique ID to the class, the I mentioned earlier. The reason i’m doing this, is because Ionic sometimes gets confused and will mute/unmute
the audio on ALL videos, so we use this to specify which video we want to be interacting with.
Next, we’re going to create two functions. One will take care of changing our video audio — which will be muted by default, and the second will handle playing & pausing the video as we scroll.
Function — changeVideoAudio(id: string)
public changeVideoAudio(id: string) {
let vid: any = document.getElementById('media-' + id);
vid.muted = !vid.muted;
}
This function is aptly named changeVideoAudio
, because that’s all it does. When a video starts playing in our app, by default, it’ll be muted. This is just standard good UX and if you don’t do this by default, your user-base will probably hate you.
So we grab the video element via document.getElementById('media-' + id);
with the video’s unique ID. media-
is just an additional unique identifier, incase we use the unique id
elsewhere.
Lastly, we’re going to access the video element (which we named vid
) and change the muted
property equal to the opposite of itself. This makes it easy for us to mute and unmute the video without having to worry about the state of the audio.
Function — onIntersection($event)
The onIntersection
function will be what is going to determine whether the video should be paused or playing as the user scrolls. It’s pretty straight forward, but i’ll explain it a bit more in depth.
onIntersection($event) {
const { [InViewportMetadata]: { entry }, target } = $event;
const ratio = entry.intersectionRatio;
const vid = target;
ratio >= 0.65 ? vid.play() : vid.pause();
}
We start out by declaring an odd looking set of variables
const { [InViewportMetadata]: { entry }, target } = $event;
This will allow us to properly access the ratio
of the video in the view, as well as the target
video object itself. You can read more about why this declaration works, but I won’t be covering that in this article. For simplicities sake, just trust me that it works.
We next have two constants, these aren’t required, but I like to name my variables as something that I can quickly understand, and manipulate. Hence:
const ratio = entry.intersectionRatio;
const vid = target;
The constant called ratio
is how much the video is in our view. We’ll configure this setting a bit more in our html file, but it’ll be a value between 0 and 1 at any given time. 0 meaning that the video is not in view at all, and 1 meaning that the video is 100% in the view.
const vid = target
is just me renaming the video object from target
to vid
for clarity’s sake.
ratio >= 0.65 ? vid.play() : vid.pause();
Lastly, I’m just using a ternary operator to check if the video is greater than or equal to 65% in the view. You can play around with this number, but this is what I’ve found to be the best ratio to start & stop videos at from a User Experience perspective. If the ratio is >=
65%, then we play the video. If it’s less than 65%, we pause the video.
Component — inline-video.component.html
Your html file is going to be a little bit complicated, so bear with me.
To start, we’ll add a simple parent div
element, which will allow us to change the audio of our video. The reason we’re doing this on a div, and not the actual video, is because Ionic is weird, and doesn’t like us doing it on the video element itself.
“I don’t write the framework bugs, I just find work arounds for them…”
— Jordan Benge circa 2019
<div tappable (tap)="changeVideoAudio(video?.id)">
...
</div>
This is pretty self explanatory, we’re just passing the video id
to our changeVideoAudio
function we created earlier.
Next, let’s add in the video element. I’ll walk through each line, and explain what’s going on, like usual.
<div tappable (tap)="changeVideoAudio(video?.id)">
<video
inViewport
[inViewportOptions]="{ threshold: [0, 0.65], partial: true }"
(inViewportAction)="onIntersection($event)"
playsinline loop
[muted]="'muted'" preload="auto" muted="muted"
[poster]="video?.src"
[id]="'media-' + video?.id" class="video-media">
<source [src]="video.src" type="video/mp4" src="">
</video>
</div>
inViewport
lets Ionic & Angular know that we’re adding in the inViewport
directive to our video element. This is what will allow us to know if the video is in the viewport or not. 🙌 pretty neat huh?
[inViewportOptions]="{ threshold: [0, 0.65], partial: true }"
This line is required configuration for our inViewport
direction. I have the threshold set to be between 0 and 0.65 or 65%. This is because I want to know when the element enters the viewport as well as when it leaves. You can add additional values if you’d like, i’ve linked the threshold api documentation above, but this is the best setup in my opinion.
partial: true
just means that we want to know when the element partially enters the viewport. Otherwise, the element would only fire when it was 100% in the viewport, instead of 65% like we’ve configured.
(inViewportAction)="onIntersection($event)"
This is the event that fires and calls our onIntersection()
function when an element position changes in the viewport, relative to the configuration we setup above.
playsinline
— tells our app that this video should be played inline — instead of using the native video player from our device. Without this, the video would not automatically play on scroll.
loop
— just loops our video indefinitely, which is required if we want the video to repeat itself after it’s finished, without any additional user interaction.
[muted]="'muted'" preload="auto" muted="muted"
preload
— just tells the browser that we want the video to start loading & buffering when the page loads. It’s not required, but I recommend it for a faster and better UX.
[muted]/muted
— the reason i’ve included both muted
attributes is, because there is often issues with the different phone platforms where [muted]
works on one but the other. So I just include them both, for simplicities sake.
[id]="'media-' + video?.id" class="video-media">
id
— pretty typical Angular HTML stuff going on here, i’m setting the element id
to be media-
+ video?.id
, so we can grab the correct video when we’re changing the audio from muted to unmuted & vise-versa by tapping it. The class is just so I can style the video however I like.
<source [src]="video.src" type="video/mp4">
This is the video source element, and it’s pretty self explanatory. The type is important, because you could serve up different videos based on the user’s device. I just default to using video/mp4
because it’s universally supported, but you might also want to use others such as video/ogg
or video/webm
depending on your target audience.
That’s it!
If you restart your application, you should be able to see your videos auto playing as they scroll into view. If you tap it, it’ll change the audio so it’s no longer muter. Tap it again, and it’ll re-mute it! Continue scrolling past the video, and it should automatically pause it once it’s past the 65% threshold.
Pretty neat stuff, huh? 😄
Questions?
You can find me on:
- GitHub: https://github.com/bengejd/
- Medium: https://medium.com/@JordanBenge
- Twitter: https://twitter.com/J_Benge13
Who am I? My name is Jordan Benge, I am a Software Developer who loves helping others and contributing to Open-Source. I’ve been working in the Ionic Framework since Ionic 1, and have tried to keep up to date on the latest and greatest when it comes to Hybrid Mobile App Development.