The client library
One file, no build step. It uses fetch and nothing else, so the same code runs in a browser, on your server under Node 18+, and in React Native.
<script src="https://storage.vylo-tech.com/sdk/vylo.js"></script>
<script>
// In a browser, never ship a zone key. Ask your own backend for a
// short-lived token instead; the client refreshes it by itself.
const vylo = new Vylo({
zone: 'your-zone',
getToken: () => fetch('/api/vylo-token').then(r => r.json()),
});
const input = document.querySelector('input[type=file]');
input.onchange = async () => {
const result = await vylo.uploadVideo(
'w1/lecture.mp4', input.files[0],
{ onProgress: (p) => console.log(Math.round(p * 100) + '%') },
);
// uploadVideo probes it and, if no browser would play it, encodes it.
console.log('playable at', result.playableKey);
};
</script>
On your server
import { Vylo } from 'https://storage.vylo-tech.com/sdk/vylo.mjs';
const vylo = new Vylo({
zone: 'your-zone',
zoneKey: process.env.VYLO_ZONE_KEY,
});
// a link for one student, that does not work if they forward it
const link = await vylo.sign('w1/lecture.mp4', {
ttl: 3600, viewer: 'student-4182', ip: req.ip,
});
// the iframe to drop into a page
const html = vylo.embedCode(link, { autoplay: false, color: '4c8dff' });
React Native — WebView
import { WebView } from 'react-native-webview';
// mint the link on your server, never in the app
const ORIGIN = 'https://storage.vylo-tech.com';
const uri = ORIGIN + '/play/your-zone/w1/lecture.mp4?' + query;
const ref = useRef(null);
<WebView
ref={ref}
source={{ uri }}
allowsFullscreenVideo
allowsInlineMediaPlayback // iOS: or it goes fullscreen-native
mediaPlaybackRequiresUserAction={false}
onMessage={(e) => {
const m = JSON.parse(e.nativeEvent.data);
if (m.vylo === 'timeupdate') save(m.currentTime);
if (m.vylo === 'ended') markLectureWatched();
}}
/>
// to control it, inject rather than postMessage — it behaves the
// same on both platforms
ref.current.injectJavaScript(
"window.postMessage(JSON.stringify({vylo:'seek',value:90}));true;"
);
React Native — native player
import Video from 'react-native-video';
// /playable returns every rendition already signed
const { sources, poster, width, height } = await res.json();
<Video
source={{ uri: sources[0].src }}
poster={poster}
controls
resizeMode="contain"
style={{ aspectRatio: width / height }}
onEnd={markLectureWatched}
/>
The client library in an app
// The library uses fetch and nothing else, so it runs on Hermes
// unchanged — no URL, URLSearchParams, TextEncoder or btoa.
import { Vylo } from 'https://storage.vylo-tech.com/sdk/vylo.mjs';
const vylo = new Vylo({
zone: 'your-zone',
getToken: () => fetch('https://your-api/vylo-token').then(r => r.json()),
});
await vylo.list('w1/');
await vylo.sign('w1/lecture.mp4', { ttl: 3600 });
// Uploading: React Native has no File object, so turn the file URI
// into a Blob first. RN Blobs support slice, so resumable works.
const blob = await fetch(localFileUri).then(r => r.blob());
await vylo.upload('w1/lecture.mp4', blob, {
contentType: 'video/mp4',
onProgress: (p) => setProgress(p),
});
One thing not to do in an app: bind a link to an IP. A phone
moves between wifi and mobile data mid-lecture and the address changes
with it, so the link stops working halfway through. Use a short ttl
and a viewer id instead.
Controlling an embedded player
const player = new VyloPlayer('#lecture'); // the iframe
player.on('timeupdate', ({ currentTime, duration }) => save(currentTime));
player.on('ended', () => next());
player.seek(90);
player.play();
Keeping a video private
What a private zone actually guarantees, and what it does not. Worth reading once before you hand a link to anybody.
private by default
A new zone serves nothing without a signature. There is no
unlisted-but-reachable state: an unsigned request is refused whether it is a GET, a
HEAD or a byte range.
one object
A signature covers that file, that zone and that expiry together.
It does not work on the file beside it, the same path in another zone, or a second
past its time — and moving the expiry breaks it.
viewer
Adds an id of yours to the link, covered by the signature. It does
not stop the link being forwarded; it makes a forwarded one say whose it was, which is
usually what ends the habit.
ip
Stops it being forwarded. The same URL from another network does
not play. The cost is real: a phone moving from wifi to mobile data changes address
and the link stops there too, so use it where that is acceptable.
short ttl
The simplest control and the one people skip. Mint a link when
somebody presses play, not when the page loads.
not DRM
Anyone who can watch a video can record it. This makes a link
useless to somebody it was not issued to; it cannot make a file unwatchable by a
person it was.