Last push of documentation for the client stuff.

main
Zed A. Shaw 2 years ago
parent 2192525c7c
commit 14b7337949
  1. 5
      client/main.js
  2. 31
      client/stores.js
  3. 23
      client/websocket.js
  4. 94
      client/wt.js

@ -1,3 +1,8 @@
/*
This is the main file for the client Svelte app. Usually you wouldn't need to
modify this unless you want to alter the Svelte configuration or add some
necessary code.
*/
import App from '$/client/App.svelte'; import App from '$/client/App.svelte';
const app = new App({ const app = new App({

@ -1,11 +1,28 @@
/*
The main Svelte stores used in the application. The main one is the `user`, which
contains a store for the current user. Svelte stores are a useful way to maintain
information that's used across pages. I've left in a few that I used for caching
in my courses at https://learnjsthehardway.com so you can see how to use a store
for caching.
*/
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
/*
// look in api/login.js and client/pages/Login.svelte for where this is initially setup Look in api/login.js and client/pages/Login.svelte for where this is initially setup.
*/
export const user = writable({ export const user = writable({
authenticated: undefined, // yes, this is on purpose authenticated: undefined, // yes, this is on purpose
}); });
/*
I used this for some basic caching of course items, since constantly loading
them on every click was pointless. The course material doesn't change much,
so caching it makes sense. Why didn't I use Service Workers instead? Service
Workers are very unreliable when it comes to reloading. They require a lot of
extra gear just to make simple refreshing a page of content work. If what you
cache persists across refresh the go ahead and use Service Worker caching. If
not then just do it this way.
*/
export const cache = writable({ export const cache = writable({
courses: {}, courses: {},
modules: {}, modules: {},
@ -13,6 +30,9 @@ export const cache = writable({
recents: {} recents: {}
}); });
/*
Clears out the cache. Again, mostly a demo.
*/
export const cache_reset = () => { export const cache_reset = () => {
cache.update(() => ({ cache.update(() => ({
courses: {}, courses: {},
@ -22,6 +42,13 @@ export const cache_reset = () => {
})); }));
} }
/*
Use for video configuration information that's shared between
video pages.
*/
export const video_config = writable({video_ready: false}); export const video_config = writable({video_ready: false});
/*
The user's darkmode setting.
*/
export const darkmode = writable({theme: "light"}); export const darkmode = writable({theme: "light"});

@ -1,12 +1,27 @@
/*
Helpers for dealing with the `socket/` handlers, similar to the
code in `client/api.js`. WebSockets already work well with JSON
data, so this mostly handles create a single connection, and
reconnecting when you need to.
*/
import { inject_remote } from "$/client/components/Source.svelte"; import { inject_remote } from "$/client/components/Source.svelte";
let raw_socket; let raw_socket;
/*
Load the `/js/socket.io.min.js` file using `client/components/Sourse.svelte:inject_remote`.
This makes it so if you don't use the socket.io stuff then you won't download the code.
*/
export const configure_socket = async () => { export const configure_socket = async () => {
await inject_remote(document, "/js/socket.io.min.js"); await inject_remote(document, "/js/socket.io.min.js");
} }
/*
Connect to the server __once__. This should maintain the connection
in the SPA for as long as possible and return only one.
+ `reconnection boolean` -- The reconnection policy that's passed to socket.io `io()`.
*/
export const connect_socket = (reconnection=false) => { export const connect_socket = (reconnection=false) => {
if(raw_socket === undefined) { if(raw_socket === undefined) {
raw_socket = io({ reconnection }); raw_socket = io({ reconnection });
@ -15,6 +30,14 @@ export const connect_socket = (reconnection=false) => {
return raw_socket; return raw_socket;
} }
/*
This is actually the way you should connect a socket in most pages.
This will detect if a connection already exists, and if it does it
simply disconnects, then reconnects.
If there isn't a connection yet then it will call `connect_socket()`
to establish the first one.
*/
export const reconnect_socket = () => { export const reconnect_socket = () => {
if(raw_socket !== undefined) { if(raw_socket !== undefined) {
raw_socket.disconnect(); raw_socket.disconnect();

@ -1,18 +1,28 @@
/*
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; const seedTimeout = 60000;
import { webtorrent, base_host } from '$/client/config.js'; import { webtorrent, base_host } from '$/client/config.js';
import assert from "$/client/assert.js"; import assert from "$/client/assert.js";
import { log } from "$/client/logging.js"; import { log } from "$/client/logging.js";
/* This needs an entire rewrite using the FSM code. Most of this is hacked on
* garbage that is unreliable at the best of times.
*/
// 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
let LAZY_CLIENT; // this is lazy loaded by getClient 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 = () => { const getClient = () => {
// super hacks! // super hacks!
if(LAZY_CLIENT) { if(LAZY_CLIENT) {
@ -34,7 +44,20 @@ const getClient = () => {
} }
} }
/*
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 { 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) { constructor(media, video_opts) {
assert(media, "Media must exist."); assert(media, "Media must exist.");
this.media = media; this.media = media;
@ -42,12 +65,17 @@ export class WTEvents {
this.appendId = `append-${media.target_id}`; this.appendId = `append-${media.target_id}`;
} }
/* Set the torrent to use. */
setTorrent(torrent) { setTorrent(torrent) {
// TODO: refine this // TODO: refine this
this.media.torrent = torrent; this.media.torrent = torrent;
this.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) { videoReady(file) {
this.media.kind = 'video'; this.media.kind = 'video';
file.renderTo(`#${this.appendId}`, this.video_opts); file.renderTo(`#${this.appendId}`, this.video_opts);
@ -57,6 +85,10 @@ export class WTEvents {
}); });
} }
/*
Same as videoReady. This will setup HTML5 `<audio>` tags instead
of `<video>` tags.
*/
audioReady(file) { audioReady(file) {
this.media.kind = 'audio'; this.media.kind = 'audio';
file.renderTo(`#${this.appendId}`, this.video_opts); file.renderTo(`#${this.appendId}`, this.video_opts);
@ -66,6 +98,12 @@ export class WTEvents {
}); });
} }
/*
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) { otherReady(file) {
file.renderTo(`#${this.appendId}`, (err, elem) => { file.renderTo(`#${this.appendId}`, (err, elem) => {
// TODO: hmmm do we need to do anything here with elem? // TODO: hmmm do we need to do anything here with elem?
@ -84,51 +122,67 @@ export class WTEvents {
}); });
} }
/* Override this to do something when a download of the media is available. */
download_available(url) { download_available(url) {
log.debug("download url available"); log.debug("download url available");
} }
/* Override this to handle when it's done downloading. */
done() { done() {
log.debug("done", this.media); log.debug("done", this.media);
} }
/* Probably don't need to alter this. Use super if you override it. */
infoHash(hash) { infoHash(hash) {
this.media.info_hash = hash; this.media.info_hash = hash;
} }
/* Probably don't need to alter this. Use super if you override it. */
metadata(data) { metadata(data) {
this.media.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() { ready() {
return true; return true;
} }
/* Logs WebTorrent warnings. */
warning(msg) { warning(msg) {
log.debug("WT WARNING", msg); log.debug("WT WARNING", msg);
} }
/* Logs WebTorrent errors. */
error(msg) { error(msg) {
log.debug("WT ERROR", msg); log.debug("WT ERROR", msg);
} }
/* Event when bytes are downloaded. Override to process it. */
download(bytes) { download(bytes) {
return bytes !== undefined; return bytes !== undefined;
} }
/* Event when bytes are uploaded. Override to process it. */
upload(bytes) { upload(bytes) {
return bytes !== undefined; return bytes !== undefined;
} }
/* Notification when there's a new connection to ... a wire. */
wire(wire) { wire(wire) {
// this weird idiom is to make eslint shut up for these placeholders // this weird idiom is to make eslint shut up for these placeholders
return wire !== undefined; return wire !== undefined;
} }
/* WebTorrent event for ... uh... noPeers? */
noPeers(announceType) { noPeers(announceType) {
log.debug('noPeers from', announceType); log.debug('noPeers from', announceType);
} }
/* Handles any client errors, currently just logs. */
client_error(err) { client_error(err) {
log.error(err, "client error"); log.error(err, "client error");
return true; return true;
@ -136,6 +190,10 @@ export class WTEvents {
} }
/*
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) => { const handle_torrent = (media, torrent, events) => {
torrent.files.forEach((file) => { torrent.files.forEach((file) => {
media.file_name = file.name; media.file_name = file.name;
@ -164,6 +222,12 @@ const handle_torrent = (media, torrent, events) => {
torrent.on('noPeers', announceType => events.noPeers(announceType)); 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) => { export const fetch_torrent_file = async (media) => {
assert(media, "media can't be undefined"); assert(media, "media can't be undefined");
assert(media.torrent_url, "media does not have torrent_url set"); assert(media.torrent_url, "media does not have torrent_url set");
@ -180,8 +244,11 @@ export const fetch_torrent_file = async (media) => {
} }
} }
// 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 = '*'; 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) => { export const load = (media, events) => {
assert(media, "media can't be undefined"); assert(media, "media can't be undefined");
assert(media.torrent_file, "media does not have torrent_file set"); assert(media.torrent_file, "media does not have torrent_file set");
@ -194,6 +261,15 @@ export const load = (media, 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) => { export const remove = (media) => {
if(media.torrent) { if(media.torrent) {
getClient().remove(media.torrent); getClient().remove(media.torrent);

Loading…
Cancel
Save