@eyevinn/media-event-filter
v3.4.1
Published
HTML5 media event filter
Downloads
3,658
Readme
Media Event Filter
A dependecy free, tiny package for producing a sane default interpretation of playback events from a HTMLMediaElement.
Why
The HTML5 video element does not produce a consistent set of events that correspond to the updates required by trackers and user interfaces, like loaded
, buffering
or seeking
.
Different player engines built on top of the Media Source Extension all have their own implementations for listening to events.
This filter aims to provide a single source of truth that can be used across player engines and browser native playback.
Adheres to the Eyevinn Player Analytics Specification.
Overview
/** Loading of stream is complete, playback is ready to start */
/** It is now safe to let the user press a play button */
"loaded";
/** A seek has started */
"seeking";
/** A seek has ended */
"seeked";
/** Buffering has started */
/** Does not trigger during seek, cancelled by a seek */
"buffering";
/** Buffering has ended */
/** Does not trigger during seek */
"buffered";
/** A request to start playing again has been made */
"play";
/** The stream has started playing after loading completed
* OR the stream has started playing after the stream was previously paused */
"playing";
/** The stream has been paused */
"pause";
/** The end of the stream was reached */
"ended";
/** A timeupdate event */
"timeupdate";
Limitations
Does not support native HTML5 MSE controls (<video controls>
). It works, but event sequence will not strictly follow EPAS in all browsers.
Usage
npm install @eyevinn/media-event-filter
yarn add @eyevinn/media-event-filter
Example of creating and listening to the event filter.
import {
getMediaEventFilter,
FilteredMediaEvent,
} from "@eyevinn/media-event-filter";
const videoElement = document.createElement("video");
// Using a switch statement
const mediaEventFilter = getMediaEventFilter({
mediaElement: videoElement,
callback: (event: FilteredMediaEvent) => {
switch (event) {
case FilteredMediaEvent.LOADED:
// handle loaded
break;
case FilteredMediaEvent.BUFFERING:
// handle buffering
break;
case FilteredMediaEvent.BUFFERED:
// handle buffered
// ...
default:
break;
}
},
});
// Call when done
mediaEventFilter.teardown();
// Object notation can also be used
const handlers = {
[FilteredMediaEvent.LOADED]: () => {
/* handle loaded */
},
[FilteredMediaEvent.BUFFERING]: () => {
/* handle buffering */
},
[FilteredMediaEvent.BUFFERED]: () => {
/* handle buffered */
},
// ...
};
const mediaEventFilter = getMediaEventFilter({
mediaElement: videoElement,
callback: (event: FilteredMediaEvent) => handlers[event]?.(),
});
// It is safe to use destructuring
const { teardown } = getMediaEventFilter({
/* ... */
});
teardown();
Sample Shaka + React Component
The filter can be used to easily build a React UI on top of Shaka.
A barebones sample integration (see it on codepen):
import { useCallback, useEffect, useMemo, useRef, useState } from "React";
import shaka from "shaka-player";
import {
FilteredMediaEvent,
getMediaEventFilter,
} from "@eyevinn/media-event-filter";
const PlayerComponent = ({ videoUrl }) => {
const videoRef = useRef(null);
const [playing, setPlaying] = useState(false);
const [loading, setLoading] = useState(true);
const [blocked, setBlocked] = useState(false);
useEffect(() => {
if (!videoUrl || !videoRef.current) return () => {};
const eventFilter = getMediaEventFilter({
mediaElement: videoRef.current,
// add your state handlers here
callback: (event) => {
switch (event) {
case FilteredMediaEvent.LOADED:
setLoading(false);
// attempt autoplay
videoRef.current.play().catch((e) => {
// catch autoplay block
if (e.name.indexOf("NotAllowedError") > -1) {
setBlocked(true);
}
});
break;
case FilteredMediaEvent.PLAYING:
// reset autplay blocked
setBlocked(false);
// we're playing!
setPlaying(true);
break;
case FilteredMediaEvent.ENDED:
case FilteredMediaEvent.PAUSE:
setPlaying(false);
break;
default:
break;
}
},
});
const player = new shaka.Player(videoRef.current);
// Add configuration if needed
// player.configure()
player
// start loading the stream
.load(videoUrl)
// catch errors during load
.catch(console.error);
// Kill player when unmounted
return () => {
player.destroy();
eventFilter.teardown();
};
}, [videoUrl, videoRef]);
const play = useCallback(() => {
if (!videoRef.current) return;
videoRef.current.play();
}, [videoRef]);
const pause = useCallback(() => {
if (!videoRef.current) return;
videoRef.current.pause();
}, [videoRef]);
return (
<div style={{ width: "720px", margin: "20px auto" }}>
{loading && <p>Video is Loading</p>}
{!loading &&
(playing ? (
<button type="button" onClick={pause}>
Pause
</button>
) : (
<button type="button" onClick={play}>
Play
</button>
))}
{blocked && <p>Autoplay blocked, please start playback manually</p>}
<video ref={videoRef} style={{ width: "100%", height: "auto" }} />
</div>
);
};
// Use it:
<PlayerComponent videoUrl="https://bitmovin-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd" />
Benefits
Get a single source of truth for playback events regardless of engine (Shaka, Hls.js, DashJS, native) or browser (Chrome, Firefox, Safari).
Pipe the events directly into your UI state management for a reliable source of truth of playback state updates.
The event sequence map directly to popular tracking providers and playback SDKs without further filtering, like Youbora, Comscore, Yospace, or Nielsen. If yours is also supported we welcome PRs to update this list!
Compatible with EPAS.
Events
A description of events and their sequencing.
Example Sequences
LOADED; // playback is ready to start
PLAYING; // playback started
SEEKING; // seek requested
PAUSED; // manual pause
PLAY; // manual play
SEEKED; // seek finished
PLAYING; // video is rolling again
BUFFERING; // unable to continue playing due to missing buffer
BUFFERED; // buffer ended by incoming seek request
SEEKING; // seek requested
SEEKED; // seek finished
PAUSED; // manual pause
PLAY; // manual play
PLAYING; // video is rolling again
ENDED; // video reached the end
loaded
The initial load of the video has completed, and it is ready to start playing.
No other event can trigger before loaded.
seeking
Seeking has started.
If buffering is ongoing buffered will be triggered prior to seeking.
Buffer events can not trigger during a seek.
Can not trigger before loaded.
seeked
Seeking has ended.
Can not trigger before loaded.
Can not trigger without a preceding seeking event.
buffering
Buffering has started.
Can not trigger during a seek.
Can not trigger before loaded.
buffered
Buffering has ended, or was interrupted by a seek.
Can not trigger during a seek.
Can not trigger without a preceding buffering event.
play
Playback has been requested.
Can not trigger before loaded.
Can not trigger if video was not previously paused.
playing
Playback has started.
Can not trigger before loaded.
Can not trigger during seeking.
Can not trigger during buffering.
Can not trigger if video was not previously paused.
A play requested during seeking or buffering will trigger playing after the seek or buffer has finished.
pause
Playback has been paused.
Seeking or buffering do not count as pausing.
Can not trigger before loaded.
timeupdate
A timeupdate event
Can not trigger before loaded.
ended
The player has reached the end of a stream.
To allow restarting the stream after the end of stream has been reached set allowResumeAfterEnded
to true
.
Contributing
Contributions to improve compatibility with or add support for different engines, tracking solutions, and browsers are welcome.
Git Ways of Working
The project uses feature branches, and a rebase merge strategy.
Make sure you have git pull
set to rebase mode:
git config pull.rebase true
Optionally, you can add the --global
flag to the above command.
To start working on a new feature: git checkout <feature branch name>
.
As the project uses semantic-release to automatically generate release notes based on commits, it is important to follow some rules when committing.
This project uses conventional commits.
Read Using Git with Discipline.
Read How to Write a Commit Message.
A commit should:
- contain a single change set (smaller commits are better)
- pass tests, linting, and typescript checks
- not be broken
Along with enabling time saving automation, it enables extremely powerful debug workflows via git bisect, making bug hunting a matter of minutes instead of days. There are a number of articles out there on the magic of bisecting.
Basic structure of a commit message:
<type>[optional scope]: <title starting with verb in infinitive>
[optional body]
[optional footer]
For automated release notes to work well, try to describe what was added or changed, instead of describing what the code does. Example:
fix(seek): rewrite calculation in seek module
// bad, the consumer does not know what issue this fixes
fix(seek): stop player from freezing after seek
// good, the consumer understands what is now working again
Develop
To start a dev server: pnpm dev
, check demo
and public/index.html
for details.
Familiarity with the HTML5 video standard, shaka, hlsjs, or other engines is recommended before contributing.
https://www.w3.org/TR/2011/WD-html5-20110113/video.html
https://html.spec.whatwg.org/multipage/media.html
https://html.spec.whatwg.org/multipage/media.html#mediaevents
Tested with shaka 2.5.X - 4.X.X
Tested with native video in Safari
Tested with hls.js
Tested on Safari 13.1+, Firefox, Chrome, Edge
Tested on OSX, Windows, Linux
Releasing
Releases are triggered via a github action that will automatically increment the version and write a changelog based on commits.
Manual releases can be made by running pnpm release
.
Support
Join our community on Slack where you can post any questions regarding any of our open source projects. Eyevinn's consulting business can also offer you:
- Further development of this component
- Customization and integration of this component into your platform
- Support and maintenance agreement
Contact [email protected] if you are interested.
About Eyevinn Technology
Eyevinn Technology is an independent consultant firm specialized in video and streaming. Independent in a way that we are not commercially tied to any platform or technology vendor. As our way to innovate and push the industry forward we develop proof-of-concepts and tools. The things we learn and the code we write we share with the industry in blogs and by open sourcing the code we have written.
Want to know more about Eyevinn and how it is to work here. Contact us at [email protected]!