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

/*
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 };