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