This is the code that runs https://bandolier.learnjsthehardway.com/ for you to review. It uses the https://git.learnjsthehardway.com/learn-javascript-the-hard-way/bandolier-template to create the documentation for the project.
https://bandolier.learnjsthehardway.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
281 lines
8.6 KiB
281 lines
8.6 KiB
/*
|
|
This needs an entire rewrite using the FSM code. Most of this is hacked on
|
|
garbage that is unreliable at the best of times. The documentation here is
|
|
mostly give what I have a bit of help, and I keep this _mostly_ as an example
|
|
of how to work with an older API. In the future I want to rewrite this--or write
|
|
an alternative--that uses the `client/fsm.js` library similar to how `HLSVideo.svelte`
|
|
works.
|
|
*/
|
|
|
|
const seedTimeout = 60000;
|
|
import { webtorrent, base_host } from '$/client/config.js';
|
|
import assert from "$/client/assert.js";
|
|
import { log } from "$/client/logging.js";
|
|
|
|
|
|
let LAZY_CLIENT; // this is lazy loaded by getClient
|
|
|
|
/*
|
|
Load the massive WebTorrent client javascript.
|
|
|
|
___WARNING___: having svelte build the webtorrent client into the script
|
|
makes it slow to load and massive. it's much faster to set it as a script
|
|
variable in the index.html but that might cause loading problems WebTorrent
|
|
then comes from the public/index.html file, not from an import in here
|
|
*/
|
|
const getClient = () => {
|
|
// super hacks!
|
|
if(LAZY_CLIENT) {
|
|
return LAZY_CLIENT;
|
|
} else {
|
|
// eslint will complain but webtorrent is loaded by <script>
|
|
let client = new WebTorrent(webtorrent);
|
|
|
|
// would need to do some kind of global status for these since it's not per torrent
|
|
if(webtorrent.rate !== undefined) {
|
|
client.throttleDownload(webtorrent.rate);
|
|
client.throttleUpload(webtorrent.rate);
|
|
}
|
|
|
|
client.on('error', err => log.error(err));
|
|
client.on('listening', () => log.debug('LISTENING:'));
|
|
LAZY_CLIENT = client;
|
|
return LAZY_CLIENT;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Base class for handling WebTorrent events. Look at the `VideoEvents` class
|
|
in `client/components/WTVideo.svelte` for how to use it. You subclass this
|
|
if you want to give visual feedback to different events in WebTorrent, such
|
|
as new clients connecting, download rates, etc.
|
|
*/
|
|
export class WTEvents {
|
|
/*
|
|
Takes a `Media` object and video player options, then sets up the
|
|
necessary things to make a video play after WebTorrent has downloaded it.
|
|
|
|
+ `media Media` -- Media object from the database.
|
|
+ `video_opts Object` -- Default is `{autoplay: false, controls: true, muted: false}`.
|
|
*/
|
|
constructor(media, video_opts) {
|
|
assert(media, "Media must exist.");
|
|
this.media = media;
|
|
this.video_opts = video_opts || {autoplay: false, controls: true, muted: false}
|
|
this.appendId = `append-${media.target_id}`;
|
|
}
|
|
|
|
/* Set the torrent to use. */
|
|
setTorrent(torrent) {
|
|
// TODO: refine this
|
|
this.media.torrent = torrent;
|
|
this.torrent = torrent;
|
|
}
|
|
|
|
/*
|
|
Does the work needed when a video is ready.
|
|
Be sure to call it with super if you override.
|
|
*/
|
|
videoReady(file) {
|
|
this.media.kind = 'video';
|
|
file.renderTo(`#${this.appendId}`, this.video_opts);
|
|
file.getBlobURL((err, url) => {
|
|
if(err) log.error(err, "getBlobURL error videoReady");
|
|
this.download_available(url);
|
|
});
|
|
}
|
|
|
|
/*
|
|
Same as videoReady. This will setup HTML5 `<audio>` tags instead
|
|
of `<video>` tags.
|
|
*/
|
|
audioReady(file) {
|
|
this.media.kind = 'audio';
|
|
file.renderTo(`#${this.appendId}`, this.video_opts);
|
|
file.getBlobURL((err, url) => {
|
|
if(err) log.error(err, "getBlobURL error audioReady");
|
|
this.download_available(url);
|
|
});
|
|
}
|
|
|
|
/*
|
|
Other kinds of media that WebTorrent will download. This doesn't
|
|
reall do much other than call `this.download_available()`. You
|
|
probably just want to override `download_available()` then
|
|
use that to make a download link.
|
|
*/
|
|
otherReady(file) {
|
|
file.renderTo(`#${this.appendId}`, (err, elem) => {
|
|
// TODO: hmmm do we need to do anything here with elem?
|
|
if(err) {
|
|
log.error(err);
|
|
} else {
|
|
// keep this active for 60 second
|
|
let client = getClient();
|
|
setTimeout(() => client.remove(this.torrent.infoHash), seedTimeout);
|
|
}
|
|
});
|
|
|
|
file.getBlobURL((err, url) => {
|
|
if(err) log.error(err, "getBlobURL error");
|
|
this.download_available(url);
|
|
});
|
|
}
|
|
|
|
/* Override this to do something when a download of the media is available. */
|
|
download_available(url) {
|
|
log.debug("download url available");
|
|
}
|
|
|
|
/* Override this to handle when it's done downloading. */
|
|
done() {
|
|
log.debug("done", this.media);
|
|
}
|
|
|
|
/* Probably don't need to alter this. Use super if you override it. */
|
|
infoHash(hash) {
|
|
this.media.info_hash = hash;
|
|
}
|
|
|
|
/* Probably don't need to alter this. Use super if you override it. */
|
|
metadata(data) {
|
|
this.media.metadata = data;
|
|
}
|
|
|
|
/*
|
|
Just returns true to WebTorrent, but you can override this if you
|
|
want to do something before WebTorrent starts. Your version
|
|
must return true.
|
|
*/
|
|
ready() {
|
|
return true;
|
|
}
|
|
|
|
/* Logs WebTorrent warnings. */
|
|
warning(msg) {
|
|
log.debug("WT WARNING", msg);
|
|
}
|
|
|
|
/* Logs WebTorrent errors. */
|
|
error(msg) {
|
|
log.debug("WT ERROR", msg);
|
|
}
|
|
|
|
/* Event when bytes are downloaded. Override to process it. */
|
|
download(bytes) {
|
|
return bytes !== undefined;
|
|
}
|
|
|
|
/* Event when bytes are uploaded. Override to process it. */
|
|
upload(bytes) {
|
|
return bytes !== undefined;
|
|
}
|
|
|
|
/* Notification when there's a new connection to ... a wire. */
|
|
wire(wire) {
|
|
// this weird idiom is to make eslint shut up for these placeholders
|
|
return wire !== undefined;
|
|
}
|
|
|
|
/* WebTorrent event for ... uh... noPeers? */
|
|
noPeers(announceType) {
|
|
log.debug('noPeers from', announceType);
|
|
}
|
|
|
|
/* Handles any client errors, currently just logs. */
|
|
client_error(err) {
|
|
log.error(err, "client error");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
This is called inside WebTorrent to process a new torrent being added to
|
|
the client. You don't really call this.
|
|
*/
|
|
const handle_torrent = (media, torrent, events) => {
|
|
torrent.files.forEach((file) => {
|
|
media.file_name = file.name;
|
|
|
|
if(media.file_name.endsWith('mp4') || media.file_name.endsWith('webm')) {
|
|
events.videoReady(file);
|
|
} else if(media.file_name.endsWith('mp3')) {
|
|
events.audioReady(file);
|
|
} else {
|
|
events.otherReady(file);
|
|
}
|
|
});
|
|
|
|
// do not remove these wrapping functions. without them you'll have the classic JS error
|
|
// of this inside the events class being randomly set to torrent or whatever it wants.
|
|
// You have to call them inside a closure like this to make sure events.this doesn't change.
|
|
torrent.on('done', () => events.done());
|
|
torrent.on('infoHash', hash => events.infoHash(hash));
|
|
torrent.on('metadata', data => events.metadata(data));
|
|
torrent.on('ready', () => events.ready());
|
|
torrent.on('warning', warning => events.warning(warning));
|
|
torrent.on('error', msg => events.error(msg));
|
|
torrent.on('download', bytes => events.download(bytes));
|
|
torrent.on('upload', bytes => events.upload(bytes));
|
|
torrent.on('wire', wire => events.wire(wire));
|
|
torrent.on('noPeers', announceType => events.noPeers(announceType));
|
|
}
|
|
|
|
/*
|
|
Used in `client/components/WTVideo.svelte` to fetch the correct .torrent file
|
|
for the media object.
|
|
|
|
+ `media Media` -- This is a `lib/models.js:Media` object.
|
|
*/
|
|
export const fetch_torrent_file = async (media) => {
|
|
assert(media, "media can't be undefined");
|
|
assert(media.torrent_url, "media does not have torrent_url set");
|
|
|
|
media.full_torrent_url = `${base_host}${media.torrent_url}`;
|
|
|
|
let res = await fetch(media.full_torrent_url, { credentials: "same-origin" });
|
|
|
|
if(res.status === 200) {
|
|
media.torrent_file = await res.blob();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
This thing's job is to load the torrents with webtorrent and then fill out the active_media.
|
|
|
|
Debug in the browser with: localStorage.debug = '*';
|
|
*/
|
|
export const load = (media, events) => {
|
|
assert(media, "media can't be undefined");
|
|
assert(media.torrent_file, "media does not have torrent_file set");
|
|
|
|
let client = getClient();
|
|
|
|
client.add(media.torrent_file, {private: webtorrent.private, withCredentials: true}, (torrent) => {
|
|
events.setTorrent(torrent);
|
|
handle_torrent(media, torrent, events);
|
|
});
|
|
}
|
|
|
|
/*
|
|
WebTorrent does a weird internal client management thing that really
|
|
is not friendly with an SPA. To manage it, I have a single WebTorrent
|
|
and then you can add or remove media to it. This is how you remove
|
|
any media that's active. Use it when people transition off of a page
|
|
showing a video so that it stops eating RAM.
|
|
|
|
+ `media Media` -- Media object from the database.
|
|
*/
|
|
export const remove = (media) => {
|
|
if(media.torrent) {
|
|
getClient().remove(media.torrent);
|
|
} else {
|
|
log.error("remove called with a media object without a torrent", media);
|
|
}
|
|
}
|
|
|
|
export default { load, remove, fetch_torrent_file, WTEvents };
|
|
|