mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-07 06:38:40 +00:00
added better markdown view pages and enabled subscription button to know when your logged in
This commit is contained in:
230
about.html
230
about.html
@@ -1,230 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>bitvid | About</title>
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="bitvid | About" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="View and render markdown content dynamically."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
|
||||
/>
|
||||
<meta property="og:url" content="https://bitvid.btc.us" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- App Icons -->
|
||||
<link rel="icon" href="assets/favicon.ico" sizes="any" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="assets/png/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="assets/png/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="assets/png/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Main Layout Styles -->
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
|
||||
<!-- Markdown-Specific Styles -->
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div
|
||||
id="app"
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo links back to index.html (or "/") -->
|
||||
<a href="index.html">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Content Section -->
|
||||
<main>
|
||||
<!--
|
||||
We give this section a white background and a shadow
|
||||
just like you originally had for other cards.
|
||||
-->
|
||||
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto pb-8 text-center px-4">
|
||||
<a
|
||||
href="http://bitvid.network/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.network
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.btc.us"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.btc.us
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.eth.limo"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.eth.limo
|
||||
</a>
|
||||
<div class="mt-2 space-x-4">
|
||||
<a
|
||||
href="community-guidelines.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Community Guidelines
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/PR0M3TH3AN/bitvid"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Nostr
|
||||
</a>
|
||||
<a
|
||||
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="getting-started.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Getting Started
|
||||
</a>
|
||||
<a
|
||||
href="about.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="roadmap.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="https://beta.bitvid.network/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Beta
|
||||
</a>
|
||||
<a
|
||||
href="torrent/beacon.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
βeacon
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
|
||||
>
|
||||
IPNS:
|
||||
<a href="ipns.html" class="text-blue-600 underline">
|
||||
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Marked.js (for converting markdown to HTML) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- Highlight.js (optional for code block highlighting) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
|
||||
<script>
|
||||
async function loadMarkdown() {
|
||||
const response = await fetch("content/about.md");
|
||||
if (response.ok) {
|
||||
const markdown = await response.text();
|
||||
const container = document.getElementById("markdown-container");
|
||||
|
||||
// Convert markdown to HTML
|
||||
const html = marked.parse(markdown);
|
||||
|
||||
// Insert the HTML into the container
|
||||
container.innerHTML = html;
|
||||
|
||||
// (Optional) Highlight code blocks
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} else {
|
||||
document.getElementById("markdown-container").innerHTML =
|
||||
'<p class="text-red-500">Error loading content. Please try again later.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
loadMarkdown();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -1,230 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>bitvid | About</title>
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="bitvid | Community Guidelines" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="View and render markdown content dynamically."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
|
||||
/>
|
||||
<meta property="og:url" content="https://bitvid.btc.us" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- App Icons -->
|
||||
<link rel="icon" href=.ico" sizes="any" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="assets/png/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="assets/png/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="assets/png/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Main Layout Styles -->
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
|
||||
<!-- Markdown-Specific Styles -->
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div
|
||||
id="app"
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo links back to index.html (or "/") -->
|
||||
<a href="index.html">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Content Section -->
|
||||
<main>
|
||||
<!--
|
||||
We give this section a white background and a shadow
|
||||
just like you originally had for other cards.
|
||||
-->
|
||||
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto pb-8 text-center px-4">
|
||||
<a
|
||||
href="http://bitvid.network/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.network
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.btc.us"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.btc.us
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.eth.limo"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.eth.limo
|
||||
</a>
|
||||
<div class="mt-2 space-x-4">
|
||||
<a
|
||||
href="community-guidelines.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Community Guidelines
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/PR0M3TH3AN/bitvid"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Nostr
|
||||
</a>
|
||||
<a
|
||||
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="getting-started.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Getting Started
|
||||
</a>
|
||||
<a
|
||||
href="about.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="roadmap.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="https://beta.bitvid.network/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Beta
|
||||
</a>
|
||||
<a
|
||||
href="torrent/beacon.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
βeacon
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
|
||||
>
|
||||
IPNS:
|
||||
<a href="ipns.html" class="text-blue-600 underline">
|
||||
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Marked.js (for converting markdown to HTML) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- Highlight.js (optional for code block highlighting) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
|
||||
<script>
|
||||
async function loadMarkdown() {
|
||||
const response = await fetch("content/community-guidelines.md");
|
||||
if (response.ok) {
|
||||
const markdown = await response.text();
|
||||
const container = document.getElementById("markdown-container");
|
||||
|
||||
// Convert markdown to HTML
|
||||
const html = marked.parse(markdown);
|
||||
|
||||
// Insert the HTML into the container
|
||||
container.innerHTML = html;
|
||||
|
||||
// (Optional) Highlight code blocks
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} else {
|
||||
document.getElementById("markdown-container").innerHTML =
|
||||
'<p class="text-red-500">Error loading content. Please try again later.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
loadMarkdown();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -30,8 +30,9 @@
|
||||
<span class="sidebar-text">Explore</span>
|
||||
</a>
|
||||
<a
|
||||
id="subscriptionsLink"
|
||||
href="#view=subscriptions"
|
||||
class="flex items-center py-2 px-4 hover:bg-gray-700 rounded font-semibold"
|
||||
class="hidden flex items-center py-2 px-4 hover:bg-gray-700 rounded font-semibold"
|
||||
>
|
||||
<img
|
||||
src="assets/svg/subscriptions-icon.svg"
|
||||
@@ -50,7 +51,7 @@
|
||||
<div class="p-4">
|
||||
<nav class="space-y-2 text-sm opacity-70">
|
||||
<a
|
||||
href="about.html"
|
||||
href="#view=about"
|
||||
class="flex items-center hover:bg-gray-800 px-4 py-2 rounded"
|
||||
>
|
||||
<img
|
||||
@@ -61,7 +62,7 @@
|
||||
<span class="sidebar-text">About</span>
|
||||
</a>
|
||||
<a
|
||||
href="community-guidelines.html"
|
||||
href="#view=community-guidelines"
|
||||
class="flex items-center hover:bg-gray-800 px-4 py-2 rounded"
|
||||
>
|
||||
<img
|
||||
@@ -72,7 +73,7 @@
|
||||
<span class="sidebar-text">Guidelines</span>
|
||||
</a>
|
||||
<a
|
||||
href="getting-started.html"
|
||||
href="#view=getting-started"
|
||||
class="flex items-center hover:bg-gray-800 px-4 py-2 rounded"
|
||||
>
|
||||
<img
|
||||
@@ -109,9 +110,7 @@
|
||||
<span class="sidebar-text">Nostr</span>
|
||||
</a>
|
||||
<a
|
||||
href="roadmap.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="#view=roadmap"
|
||||
class="flex items-center hover:bg-gray-800 px-4 py-2 rounded"
|
||||
>
|
||||
<img
|
||||
@@ -161,9 +160,7 @@
|
||||
<span class="sidebar-text">Beta</span>
|
||||
</a>
|
||||
<a
|
||||
href="ipns.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="#view=ipns"
|
||||
class="flex items-center hover:bg-gray-800 px-4 py-2 rounded"
|
||||
>
|
||||
<img
|
||||
|
@@ -1,7 +1,3 @@
|
||||

|
||||
|
||||
# About bitvid
|
||||
|
||||
Welcome to bitvid, a new kind of video platform that puts you in control. Unlike traditional video sites that keep your content on their servers, bitvid lets videos flow directly between creators and viewers. Think of it like a digital potluck where everyone brings and shares content directly with each other!
|
||||
|
||||
## What Makes bitvid Different?
|
||||
|
@@ -1,5 +1,3 @@
|
||||
# **bitvid Community Guidelines**
|
||||
|
||||
Welcome to **bitvid**, a decentralized video-sharing platform built on Nostr. These Community Guidelines outline the types of content allowed and prohibited on the platform. As bitvid is still in early access, enforcement will occur at the client level, meaning violations will result in content being blocked from display rather than removed from relays. These policies will evolve as we implement robust user blocking and reporting features.
|
||||
|
||||
## **1. Content Principles**
|
||||
|
@@ -1,5 +1,3 @@
|
||||
# Getting Started with bitvid
|
||||
|
||||
Ready to jump in? Here's everything you need to know to start watching and sharing videos on bitvid.
|
||||
|
||||
## Watching Videos
|
||||
|
@@ -1,5 +1,3 @@
|
||||
# Roadmap and Bug List
|
||||
|
||||
## UI Enhancements
|
||||
|
||||
- Add a copy Magnet button labeled "Seed".
|
||||
@@ -13,10 +11,6 @@
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- Fix public key wrapping issue on smaller screens.
|
||||
- Fix video editing failures.
|
||||
- Resolve issue where reopening the same video doesn't work after closing the video player.
|
||||
- Address "Video playback error: MEDIA_ELEMENT_ERROR: Empty src attribute" error.
|
||||
- Fix "Dev Mode" publishing "Live Mode" notes—add a flag for dev mode posts.
|
||||
|
||||
## Feature Additions
|
||||
|
@@ -1,226 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>bitvid | Getting Started</title>
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="bitvid | Getting Started" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="View and render markdown content dynamically."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
|
||||
/>
|
||||
<meta property="og:url" content="https://bitvid.network" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- App Icons -->
|
||||
<link rel="icon" href="assets/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="assets/png/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="assets/png/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Main Layout Styles -->
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
|
||||
<!-- Markdown-Specific Styles -->
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div
|
||||
id="app"
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo links back to index.html (or "/") -->
|
||||
<a href="index.html">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Content Section -->
|
||||
<main>
|
||||
<!--
|
||||
We give this section a white background and a shadow
|
||||
just like you originally had for other cards.
|
||||
-->
|
||||
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto pb-8 text-center px-4">
|
||||
<a
|
||||
href="http://bitvid.network/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.network
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.btc.us"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.btc.us
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.eth.limo"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.eth.limo
|
||||
</a>
|
||||
<div class="mt-2 space-x-4">
|
||||
<a
|
||||
href="community-guidelines.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Community Guidelines
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/PR0M3TH3AN/bitvid"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Nostr
|
||||
</a>
|
||||
<a
|
||||
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="getting-started.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Getting Started
|
||||
</a>
|
||||
<a
|
||||
href="about.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="roadmap.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="https://beta.bitvid.network/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Beta
|
||||
</a>
|
||||
<a
|
||||
href="torrent/beacon.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
βeacon
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
|
||||
>
|
||||
IPNS:
|
||||
<a href="ipns.html" class="text-blue-600 underline">
|
||||
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Marked.js (for converting markdown to HTML) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- Highlight.js (optional for code block highlighting) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
|
||||
<script>
|
||||
async function loadMarkdown() {
|
||||
const response = await fetch("content/getting-started.md");
|
||||
if (response.ok) {
|
||||
const markdown = await response.text();
|
||||
const container = document.getElementById("markdown-container");
|
||||
|
||||
// Convert markdown to HTML
|
||||
const html = marked.parse(markdown);
|
||||
|
||||
// Insert the HTML into the container
|
||||
container.innerHTML = html;
|
||||
|
||||
// (Optional) Highlight code blocks
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} else {
|
||||
document.getElementById("markdown-container").innerHTML =
|
||||
'<p class="text-red-500">Error loading content. Please try again later.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
loadMarkdown();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -41,6 +41,7 @@
|
||||
<!-- Styles -->
|
||||
<link href="css/tailwind.min.css" rel="stylesheet" />
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!--
|
||||
@@ -161,5 +162,7 @@
|
||||
<script type="module" src="js/app.js"></script>
|
||||
<script type="module" src="js/disclaimer.js"></script>
|
||||
<script type="module" src="js/index.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
230
ipns.html
230
ipns.html
@@ -1,230 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>bitvid | About</title>
|
||||
|
||||
<!-- Open Graph Meta Tags -->
|
||||
<meta property="og:title" content="bitvid | IPNS" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="View and render markdown content dynamically."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://bitvid.netlify.app/assets/jpg/bitvid.jpg"
|
||||
/>
|
||||
<meta property="og:url" content="https://bitvid.btc.us" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
|
||||
<!-- App Icons -->
|
||||
<link rel="icon" href=.ico" sizes="any" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="assets/png/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="assets/png/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="assets/png/favicon-16x16.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Main Layout Styles -->
|
||||
<link href="css/style.css" rel="stylesheet" />
|
||||
|
||||
<!-- Markdown-Specific Styles -->
|
||||
<link href="css/markdown.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<div
|
||||
id="app"
|
||||
class="container mx-auto px-4 py-8 min-h-screen flex flex-col"
|
||||
>
|
||||
<!-- Header -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo links back to index.html (or "/") -->
|
||||
<a href="index.html">
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Content Section -->
|
||||
<main>
|
||||
<!--
|
||||
We give this section a white background and a shadow
|
||||
just like you originally had for other cards.
|
||||
-->
|
||||
<div id="markdown-container" class="bg-white p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-bold mb-4">Loading Content...</h2>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto pb-8 text-center px-4">
|
||||
<a
|
||||
href="http://bitvid.network/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.network
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.btc.us"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.btc.us
|
||||
</a>
|
||||
|
|
||||
<a
|
||||
href="https://bitvid.eth.limo"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
bitvid.eth.limo
|
||||
</a>
|
||||
<div class="mt-2 space-x-4">
|
||||
<a
|
||||
href="community-guidelines.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Community Guidelines
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/PR0M3TH3AN/bitvid"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://primal.net/p/npub13yarr7j6vjqjjkahd63dmr27curypehx45ucue286ac7sft27y0srnpmpe"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Nostr
|
||||
</a>
|
||||
<a
|
||||
href="https://habla.news/p/nprofile1qyv8wumn8ghj7un9d3shjtnndehhyapwwdhkx6tpdsq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7qgdwaehxw309ahx7uewd3hkcqgswaehxw309ahx7um5wgh8w6twv5q3yamnwvaz7tm0venxx6rpd9hzuur4vgqzpzf6x8a95eyp99dmwm4zmkx4a3cxgrnwdtfe3ej504m3aqjk4ugldyww3a"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Blog
|
||||
</a>
|
||||
<a
|
||||
href="getting-started.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Getting Started
|
||||
</a>
|
||||
<a
|
||||
href="about.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
About
|
||||
</a>
|
||||
<a
|
||||
href="roadmap.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<a
|
||||
href="https://beta.bitvid.network/"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Beta
|
||||
</a>
|
||||
<a
|
||||
href="torrent/beacon.html"
|
||||
class="text-gray-500 hover:text-gray-400 transition-colors duration-200"
|
||||
target="_self"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
βeacon
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mt-2 text-xs text-gray-600 font-mono break-all max-w-full overflow-hidden"
|
||||
>
|
||||
IPNS:
|
||||
<a href="ipns.html" class="text-blue-600 underline">
|
||||
k51qzi5uqu5dgwr4oejq9rk41aoe9zcupenby6iqecsk5byc7rx48uecd133a1
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Marked.js (for converting markdown to HTML) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<!-- Highlight.js (optional for code block highlighting) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
||||
|
||||
<script>
|
||||
async function loadMarkdown() {
|
||||
const response = await fetch("content/ipns.md");
|
||||
if (response.ok) {
|
||||
const markdown = await response.text();
|
||||
const container = document.getElementById("markdown-container");
|
||||
|
||||
// Convert markdown to HTML
|
||||
const html = marked.parse(markdown);
|
||||
|
||||
// Insert the HTML into the container
|
||||
container.innerHTML = html;
|
||||
|
||||
// (Optional) Highlight code blocks
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} else {
|
||||
document.getElementById("markdown-container").innerHTML =
|
||||
'<p class="text-red-500">Error loading content. Please try again later.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
loadMarkdown();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
38
js/app.js
38
js/app.js
@@ -110,6 +110,9 @@ class bitvidApp {
|
||||
this.copyMagnetBtn = null;
|
||||
this.shareBtn = null;
|
||||
|
||||
// Hide/Show Subscriptions Link
|
||||
this.subscriptionsLink = null;
|
||||
|
||||
// Notification containers
|
||||
this.errorContainer = document.getElementById("errorContainer") || null;
|
||||
this.successContainer = document.getElementById("successContainer") || null;
|
||||
@@ -185,10 +188,19 @@ class bitvidApp {
|
||||
|
||||
// 4. Connect to Nostr
|
||||
await nostrClient.init();
|
||||
|
||||
// Grab the "Subscriptions" link by its id in the sidebar
|
||||
this.subscriptionsLink = document.getElementById("subscriptionsLink");
|
||||
|
||||
const savedPubKey = localStorage.getItem("userPubKey");
|
||||
if (savedPubKey) {
|
||||
// Auto-login if a pubkey was saved
|
||||
this.login(savedPubKey, false);
|
||||
|
||||
// If the user was already logged in, show the Subscriptions link
|
||||
if (this.subscriptionsLink) {
|
||||
this.subscriptionsLink.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Setup general event listeners, show disclaimers
|
||||
@@ -726,6 +738,11 @@ class bitvidApp {
|
||||
this.profileButton.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// Show the "Subscriptions" link if it exists
|
||||
if (this.subscriptionsLink) {
|
||||
this.subscriptionsLink.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// (Optional) load the user's own Nostr profile
|
||||
this.loadOwnProfile(pubkey);
|
||||
|
||||
@@ -772,6 +789,11 @@ class bitvidApp {
|
||||
this.profileButton.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Hide the Subscriptions link
|
||||
if (this.subscriptionsLink) {
|
||||
this.subscriptionsLink.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem("userPubKey");
|
||||
|
||||
@@ -810,31 +832,31 @@ class bitvidApp {
|
||||
* Hide the video modal.
|
||||
*/
|
||||
async hideModal() {
|
||||
// 1) Clear intervals
|
||||
// 1) Clear intervals, cleanup, etc. (unchanged)
|
||||
if (this.activeIntervals && this.activeIntervals.length) {
|
||||
this.activeIntervals.forEach((id) => clearInterval(id));
|
||||
this.activeIntervals = [];
|
||||
}
|
||||
|
||||
// *** EXPLICITLY CANCEL THE SERVICE WORKER STREAM ***
|
||||
try {
|
||||
await fetch("/webtorrent/cancel/", { mode: "no-cors" });
|
||||
} catch (err) {
|
||||
// Silently ignore if offline or the request fails
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 2) Cleanup resources (stops WebTorrent, etc.)
|
||||
await this.cleanup();
|
||||
|
||||
// 3) Hide the modal
|
||||
// 2) Hide the modal
|
||||
if (this.playerModal) {
|
||||
this.playerModal.style.display = "none";
|
||||
this.playerModal.classList.add("hidden");
|
||||
}
|
||||
this.currentMagnetUri = null;
|
||||
|
||||
// 4) Revert ?v= param in the URL
|
||||
window.history.replaceState({}, "", window.location.pathname);
|
||||
// 3) Remove only `?v=` but **keep** the hash
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("v"); // remove ?v= param
|
||||
const newUrl = url.pathname + url.search + url.hash;
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
249
js/index.js
249
js/index.js
@@ -79,6 +79,8 @@ Promise.all([
|
||||
});
|
||||
}
|
||||
|
||||
// Load and set up sidebar navigation
|
||||
// (We assume it calls setHashView(...) now)
|
||||
return import("./sidebar.js").then((module) => {
|
||||
module.setupSidebarNavigation();
|
||||
});
|
||||
@@ -145,46 +147,76 @@ Promise.all([
|
||||
});
|
||||
}
|
||||
|
||||
// 5) ?modal=appeals => open content appeals form
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const modalParam = urlParams.get("modal");
|
||||
// Once everything is loaded, handle the query params (modal? v?) & disclaimers
|
||||
handleQueryParams();
|
||||
|
||||
if (modalParam === "appeals") {
|
||||
const appealsModal = document.getElementById("contentAppealsModal");
|
||||
if (appealsModal) {
|
||||
appealsModal.classList.remove("hidden");
|
||||
}
|
||||
const closeAppealsBtn = document.getElementById(
|
||||
"closeContentAppealsModal"
|
||||
);
|
||||
if (closeAppealsBtn) {
|
||||
closeAppealsBtn.addEventListener("click", () => {
|
||||
appealsModal.classList.add("hidden");
|
||||
if (!localStorage.getItem("hasSeenDisclaimer")) {
|
||||
const disclaimerModal = document.getElementById("disclaimerModal");
|
||||
if (disclaimerModal) {
|
||||
disclaimerModal.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (modalParam === "application") {
|
||||
const appModal = document.getElementById("nostrFormModal");
|
||||
if (appModal) {
|
||||
appModal.classList.remove("hidden");
|
||||
}
|
||||
} else {
|
||||
// If there's no special param, disclaimers can show if user hasn't seen them
|
||||
const hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer");
|
||||
if (!hasSeenDisclaimer) {
|
||||
const disclaimerModal = document.getElementById("disclaimerModal");
|
||||
if (disclaimerModal) {
|
||||
disclaimerModal.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
// Listen for hash changes
|
||||
window.addEventListener("hashchange", handleHashChange);
|
||||
|
||||
// Also run once on initial load
|
||||
handleHashChange();
|
||||
});
|
||||
|
||||
/* -------------------------------------------
|
||||
HELPER FUNCTIONS FOR QUERY AND HASH
|
||||
-------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Sets the location.hash to "#view=<viewName>",
|
||||
* removing any ?modal=... or ?v=... from the query string.
|
||||
*/
|
||||
export function setHashView(viewName) {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// Remove possibly conflicting query params
|
||||
url.searchParams.delete("modal");
|
||||
url.searchParams.delete("v");
|
||||
|
||||
// Keep the existing path + updated search, set the new hash
|
||||
const newUrl = url.pathname + url.search + `#view=${viewName}`;
|
||||
|
||||
// Replace the URL so no full reload
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
|
||||
// Manually trigger handleHashChange so the view loads immediately
|
||||
handleHashChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a query param (e.g. ?modal=xxx or ?v=yyy),
|
||||
* removing any "#view=..." from the hash to avoid collisions.
|
||||
*/
|
||||
export function setQueryParam(key, value) {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// Remove any #view=... from the hash
|
||||
url.hash = "";
|
||||
|
||||
// Set the query param
|
||||
url.searchParams.set(key, value);
|
||||
|
||||
// Replace the URL
|
||||
const newUrl = url.pathname + url.search;
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
|
||||
// Immediately handle it
|
||||
handleQueryParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the current URL for ?modal=..., ?v=..., etc.
|
||||
* Open the correct modals or disclaimers as needed.
|
||||
*/
|
||||
function handleQueryParams() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const modalParam = urlParams.get("modal");
|
||||
|
||||
// 5) Check ?modal=... param (moved from original code)
|
||||
if (modalParam === "appeals") {
|
||||
const appealsModal = document.getElementById("contentAppealsModal");
|
||||
if (appealsModal) {
|
||||
appealsModal.classList.remove("hidden");
|
||||
}
|
||||
|
||||
// 6) Close content appeals modal if needed
|
||||
const closeAppealsBtn = document.getElementById("closeContentAppealsModal");
|
||||
if (closeAppealsBtn) {
|
||||
closeAppealsBtn.addEventListener("click", () => {
|
||||
@@ -192,67 +224,114 @@ Promise.all([
|
||||
if (appealsModal) {
|
||||
appealsModal.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7) Disclaimer 'I Understand' Button
|
||||
const acceptDisclaimerBtn = document.getElementById("acceptDisclaimer");
|
||||
if (acceptDisclaimerBtn) {
|
||||
acceptDisclaimerBtn.addEventListener("click", () => {
|
||||
const disclaimerModal = document.getElementById("disclaimerModal");
|
||||
if (disclaimerModal) {
|
||||
disclaimerModal.classList.add("hidden");
|
||||
// Show disclaimer if not seen
|
||||
if (!localStorage.getItem("hasSeenDisclaimer")) {
|
||||
const disclaimerModal = document.getElementById("disclaimerModal");
|
||||
if (disclaimerModal) {
|
||||
disclaimerModal.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
localStorage.setItem("hasSeenDisclaimer", "true");
|
||||
});
|
||||
}
|
||||
} else if (modalParam === "application") {
|
||||
const appModal = document.getElementById("nostrFormModal");
|
||||
if (appModal) {
|
||||
appModal.classList.remove("hidden");
|
||||
}
|
||||
} else {
|
||||
// If there's no special param, disclaimers can show if user hasn't seen them
|
||||
const hasSeenDisclaimer = localStorage.getItem("hasSeenDisclaimer");
|
||||
if (!hasSeenDisclaimer) {
|
||||
const disclaimerModal = document.getElementById("disclaimerModal");
|
||||
if (disclaimerModal) {
|
||||
disclaimerModal.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 8) Query param checks for the three new forms
|
||||
if (modalParam === "feedback") {
|
||||
// 8) Additional forms
|
||||
if (modalParam === "feedback") {
|
||||
const feedbackModal = document.getElementById("generalFeedbackModal");
|
||||
if (feedbackModal) {
|
||||
feedbackModal.classList.remove("hidden");
|
||||
}
|
||||
} else if (modalParam === "feature") {
|
||||
const featureModal = document.getElementById("featureRequestModal");
|
||||
if (featureModal) {
|
||||
featureModal.classList.remove("hidden");
|
||||
}
|
||||
} else if (modalParam === "bug") {
|
||||
const bugModal = document.getElementById("bugFixModal");
|
||||
if (bugModal) {
|
||||
bugModal.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// 9) Close buttons
|
||||
const closeFeedbackBtn = document.getElementById("closeGeneralFeedbackModal");
|
||||
if (closeFeedbackBtn) {
|
||||
closeFeedbackBtn.addEventListener("click", () => {
|
||||
const feedbackModal = document.getElementById("generalFeedbackModal");
|
||||
if (feedbackModal) {
|
||||
feedbackModal.classList.remove("hidden");
|
||||
feedbackModal.classList.add("hidden");
|
||||
}
|
||||
} else if (modalParam === "feature") {
|
||||
});
|
||||
}
|
||||
const closeFeatureBtn = document.getElementById("closeFeatureRequestModal");
|
||||
if (closeFeatureBtn) {
|
||||
closeFeatureBtn.addEventListener("click", () => {
|
||||
const featureModal = document.getElementById("featureRequestModal");
|
||||
if (featureModal) {
|
||||
featureModal.classList.remove("hidden");
|
||||
featureModal.classList.add("hidden");
|
||||
}
|
||||
} else if (modalParam === "bug") {
|
||||
});
|
||||
}
|
||||
const closeBugBtn = document.getElementById("closeBugFixModal");
|
||||
if (closeBugBtn) {
|
||||
closeBugBtn.addEventListener("click", () => {
|
||||
const bugModal = document.getElementById("bugFixModal");
|
||||
if (bugModal) {
|
||||
bugModal.classList.remove("hidden");
|
||||
bugModal.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 9) Close buttons for the new forms
|
||||
const closeFeedbackBtn = document.getElementById(
|
||||
"closeGeneralFeedbackModal"
|
||||
);
|
||||
if (closeFeedbackBtn) {
|
||||
closeFeedbackBtn.addEventListener("click", () => {
|
||||
const feedbackModal = document.getElementById("generalFeedbackModal");
|
||||
if (feedbackModal) {
|
||||
feedbackModal.classList.add("hidden");
|
||||
// You could also check ?v=someEvent to open a video, etc.
|
||||
// if you want to keep ?v= param logic here.
|
||||
// ...
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle #view=... in the hash and load the correct partial view.
|
||||
*/
|
||||
function handleHashChange() {
|
||||
const hash = window.location.hash || "";
|
||||
// Expecting something like #view=most-recent-videos
|
||||
const match = hash.match(/^#view=(.+)/);
|
||||
|
||||
// If no #view=..., load "most-recent-videos" as default
|
||||
if (!match || !match[1]) {
|
||||
import("./viewManager.js").then(({ loadView, viewInitRegistry }) => {
|
||||
loadView("views/most-recent-videos.html").then(() => {
|
||||
const initFn = viewInitRegistry["most-recent-videos"];
|
||||
if (typeof initFn === "function") {
|
||||
initFn();
|
||||
}
|
||||
});
|
||||
}
|
||||
const closeFeatureBtn = document.getElementById("closeFeatureRequestModal");
|
||||
if (closeFeatureBtn) {
|
||||
closeFeatureBtn.addEventListener("click", () => {
|
||||
const featureModal = document.getElementById("featureRequestModal");
|
||||
if (featureModal) {
|
||||
featureModal.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
const closeBugBtn = document.getElementById("closeBugFixModal");
|
||||
if (closeBugBtn) {
|
||||
closeBugBtn.addEventListener("click", () => {
|
||||
const bugModal = document.getElementById("bugFixModal");
|
||||
if (bugModal) {
|
||||
bugModal.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const viewName = match[1];
|
||||
const viewUrl = `views/${viewName}.html`;
|
||||
|
||||
// Load the partial
|
||||
import("./viewManager.js").then(({ loadView, viewInitRegistry }) => {
|
||||
loadView(viewUrl).then(() => {
|
||||
const initFn = viewInitRegistry[viewName];
|
||||
if (typeof initFn === "function") {
|
||||
initFn();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -1,31 +1,21 @@
|
||||
//js/sidebar.js
|
||||
|
||||
import { loadView } from "./viewManager.js";
|
||||
import { viewInitRegistry } from "./viewManager.js";
|
||||
// js/sidebar.js
|
||||
import { setHashView } from "./index.js"; // <--- or wherever you put it
|
||||
|
||||
export function setupSidebarNavigation() {
|
||||
// Grab all primary nav links that use the "#view=..." pattern
|
||||
const sidebarLinks = document.querySelectorAll('#sidebar a[href^="#view="]');
|
||||
sidebarLinks.forEach((link) => {
|
||||
link.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
// For a link like "#view=most-recent-videos", parse out "most-recent-videos"
|
||||
const href = link.getAttribute("href") || "";
|
||||
const viewMatch = href.match(/#view=(.+)/);
|
||||
if (!viewMatch || !viewMatch[1]) {
|
||||
return;
|
||||
}
|
||||
const viewName = viewMatch[1]; // e.g. "most-recent-videos"
|
||||
const viewUrl = `views/${viewName}.html`;
|
||||
|
||||
// Load the partial view
|
||||
loadView(viewUrl).then(() => {
|
||||
// If there's a post-load function for this view, call it
|
||||
const initFn = viewInitRegistry[viewName];
|
||||
if (typeof initFn === "function") {
|
||||
initFn();
|
||||
}
|
||||
});
|
||||
// e.g. "#view=about"
|
||||
const href = link.getAttribute("href") || "";
|
||||
const match = href.match(/^#view=(.+)/);
|
||||
if (!match) return;
|
||||
|
||||
const viewName = match[1]; // "about", "ipns", etc.
|
||||
setHashView(viewName);
|
||||
// That removes ?modal=, ?v=, sets #view=viewName,
|
||||
// and triggers handleHashChange() automatically
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -1,14 +1,32 @@
|
||||
// js/viewManager.js
|
||||
|
||||
// Load a partial view by URL into the #viewContainer
|
||||
// js/viewManager.js
|
||||
export async function loadView(viewUrl) {
|
||||
try {
|
||||
const res = await fetch(viewUrl);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load view: ${res.status}`);
|
||||
}
|
||||
const html = await res.text();
|
||||
document.getElementById("viewContainer").innerHTML = html;
|
||||
const text = await res.text();
|
||||
|
||||
// DOMParser, parse out the body, inject
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, "text/html");
|
||||
const container = document.getElementById("viewContainer");
|
||||
|
||||
container.innerHTML = doc.body.innerHTML;
|
||||
|
||||
// Now copy and execute each script
|
||||
const scriptTags = doc.querySelectorAll("script");
|
||||
scriptTags.forEach((oldScript) => {
|
||||
const newScript = document.createElement("script");
|
||||
Array.from(oldScript.attributes).forEach((attr) => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
newScript.textContent = oldScript.textContent;
|
||||
container.appendChild(newScript);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("View loading error:", err);
|
||||
document.getElementById("viewContainer").innerHTML =
|
||||
|
52
views/about.html
Normal file
52
views/about.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- views/about.html -->
|
||||
<section class="mb-8">
|
||||
<!--
|
||||
This container uses a white background and dark text,
|
||||
overriding your global “body { color: … }” rule
|
||||
-->
|
||||
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
|
||||
<h2 class="text-2xl font-bold mb-4 text-gray-700">About BitVid</h2>
|
||||
|
||||
<!--
|
||||
The .markdown-body class ensures the Markdown styling
|
||||
from your markdown.css can be applied here.
|
||||
-->
|
||||
<div id="markdown-container" class="markdown-body">
|
||||
<p class="text-gray-500">Loading content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch("content/about.md");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load about.md");
|
||||
}
|
||||
const markdownText = await response.text();
|
||||
|
||||
// Convert the markdown to HTML (requires "marked" loaded globally)
|
||||
const html = marked.parse(markdownText);
|
||||
|
||||
// Insert the HTML into the container
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// (Optional) Highlight code blocks if "hljs" is available globally
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading About content:", err);
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<p class="text-red-500">Error loading content. Please try again later.</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
53
views/community-guidelines.html
Normal file
53
views/community-guidelines.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<!-- views/community-guidelines.html -->
|
||||
<section class="mb-8">
|
||||
<!--
|
||||
This card has a white background and shadow, similar to your other partials.
|
||||
We also add text-gray-900 to override the global body text color.
|
||||
-->
|
||||
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
|
||||
<h2 class="text-2xl font-bold mb-4 text-gray-700">Community Guidelines</h2>
|
||||
|
||||
<!--
|
||||
The .markdown-body class can use your markdown.css
|
||||
so headings, paragraphs, etc. are styled nicely.
|
||||
-->
|
||||
<div id="markdown-container" class="markdown-body">
|
||||
<p class="text-gray-500">Loading content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
try {
|
||||
// Fetch the markdown file (change path if needed)
|
||||
const response = await fetch("content/community-guidelines.md");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load community-guidelines.md");
|
||||
}
|
||||
const markdownText = await response.text();
|
||||
|
||||
// Convert to HTML with Marked (make sure Marked is loaded globally in index.html)
|
||||
const html = marked.parse(markdownText);
|
||||
|
||||
// Insert into the container
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Optionally highlight code blocks if highlight.js is available globally
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading community guidelines:", err);
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<p class="text-red-500">Error loading content. Please try again later.</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
48
views/getting-started.html
Normal file
48
views/getting-started.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<!-- views/getting-started.html -->
|
||||
<section class="mb-8">
|
||||
<!-- White background, shadow, and dark text to override global site color -->
|
||||
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
|
||||
<h2 class="text-2xl font-bold mb-4 text-gray-700">Getting Started</h2>
|
||||
|
||||
<!-- .markdown-body references your markdown.css to style headings, paragraphs, etc. -->
|
||||
<div id="markdown-container" class="markdown-body">
|
||||
<p class="text-gray-500">Loading content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
try {
|
||||
// Fetch the Markdown file
|
||||
const response = await fetch("content/getting-started.md");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load getting-started.md");
|
||||
}
|
||||
|
||||
const markdownText = await response.text();
|
||||
|
||||
// Convert to HTML with Marked (assuming marked is globally available)
|
||||
const html = marked.parse(markdownText);
|
||||
|
||||
// Insert into #markdown-container
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Optional: highlight code blocks if highlight.js is loaded globally
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading 'Getting Started' content:", err);
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<p class="text-red-500">Error loading content. Please try again later.</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
52
views/ipns.html
Normal file
52
views/ipns.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- views/ipns.html -->
|
||||
<section class="mb-8">
|
||||
<!--
|
||||
Added "text-gray-900" here to ensure we override
|
||||
any global white text color that might be inherited.
|
||||
-->
|
||||
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
|
||||
<h2 class="text-2xl font-bold mb-4 text-gray-700">IPNS Gateways</h2>
|
||||
|
||||
<!--
|
||||
"markdown-body" can be any class name; it’s just so markdown.css
|
||||
can style headings, lists, etc.
|
||||
-->
|
||||
<div id="markdown-container" class="markdown-body">
|
||||
<p class="text-gray-500">Loading content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch("content/ipns.md");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load IPNS markdown file.");
|
||||
}
|
||||
const markdownText = await response.text();
|
||||
|
||||
// Convert to HTML with Marked (make sure "marked" is loaded globally)
|
||||
const html = marked.parse(markdownText);
|
||||
|
||||
// Insert final HTML into #markdown-container
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Optional: highlight code blocks (requires "hljs" loaded globally)
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading IPNS content:", err);
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<p class="text-red-500">Error loading content. Please try again later.</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
47
views/roadmap.html
Normal file
47
views/roadmap.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!-- views/roadmap.html -->
|
||||
<section class="mb-8">
|
||||
<!-- White card, dark text to override global site color -->
|
||||
<div class="bg-white p-6 rounded-lg shadow-md text-gray-900">
|
||||
<h2 class="text-2xl font-bold mb-4 text-gray-700">Roadmap</h2>
|
||||
|
||||
<!-- .markdown-body uses your markdown.css rules for headings, etc. -->
|
||||
<div id="markdown-container" class="markdown-body">
|
||||
<p class="text-gray-500">Loading content...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
try {
|
||||
// Fetch your roadmap markdown
|
||||
const response = await fetch("content/roadmap.md");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load roadmap.md");
|
||||
}
|
||||
const markdownText = await response.text();
|
||||
|
||||
// Convert markdown to HTML (marked must be globally available)
|
||||
const html = marked.parse(markdownText);
|
||||
|
||||
// Insert into the container
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Optional: highlight code blocks if highlight.js is globally available
|
||||
document.querySelectorAll("pre code").forEach((block) => {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error loading roadmap:", err);
|
||||
const container = document.getElementById("markdown-container");
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<p class="text-red-500">Error loading content. Please try again later.</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
@@ -1,977 +0,0 @@
|
||||
# WebTorrent Documentation
|
||||
|
||||
WebTorrent is a streaming torrent client for **Node.js** and the **web**. WebTorrent
|
||||
provides the same API in both environments.
|
||||
|
||||
To use WebTorrent in the browser, [WebRTC] support is required (Chrome, Firefox, Opera, Safari).
|
||||
|
||||
[webrtc]: https://en.wikipedia.org/wiki/WebRTC
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install webtorrent
|
||||
```
|
||||
|
||||
## Quick Example
|
||||
|
||||
```js
|
||||
const client = new WebTorrent();
|
||||
|
||||
const torrentId =
|
||||
"magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent";
|
||||
|
||||
// see tutorials.md for a full example of streaming media using service workers
|
||||
navigator.serviceWorker.register("sw.min.js");
|
||||
const controller = await navigator.serviceWorker.ready;
|
||||
client.createServer({ controller });
|
||||
|
||||
client.add(torrentId, (torrent) => {
|
||||
// Torrents can contain many files. Let's use the .mp4 file
|
||||
const file = torrent.files.find((file) => {
|
||||
return file.name.endsWith(".mp4");
|
||||
});
|
||||
|
||||
// Display the file by adding it to the DOM. Supports video, audio, image, etc. files
|
||||
file.streamTo(document.querySelector("video"));
|
||||
});
|
||||
```
|
||||
|
||||
# WebTorrent API
|
||||
|
||||
## `WebTorrent.WEBRTC_SUPPORT`
|
||||
|
||||
Is WebRTC natively supported in the environment?
|
||||
|
||||
```js
|
||||
if (WebTorrent.WEBRTC_SUPPORT) {
|
||||
// WebRTC is supported
|
||||
} else {
|
||||
// Use a fallback
|
||||
}
|
||||
```
|
||||
|
||||
## `client = new WebTorrent([opts])`
|
||||
|
||||
Create a new `WebTorrent` instance.
|
||||
|
||||
If `opts` is specified, then the default options (shown below) will be overridden.
|
||||
|
||||
```js
|
||||
{
|
||||
maxConns: Number, // Max number of connections per torrent (default=55)
|
||||
nodeId: String|Uint8Array, // DHT protocol node ID (default=randomly generated)
|
||||
peerId: String|Uint8Array, // Wire protocol peer ID (default=randomly generated)
|
||||
tracker: Boolean|Object, // Enable trackers (default=true), or options object for Tracker
|
||||
dht: Boolean|Object, // Enable DHT (default=true), or options object for DHT
|
||||
lsd: Boolean, // Enable BEP14 local service discovery (default=true)
|
||||
utPex: Boolean, // Enable BEP11 Peer Exchange (default=true)
|
||||
natUpnp: Boolean | String, // Enable NAT port mapping via NAT-UPnP (default=true). NodeJS only
|
||||
natPmp: Boolean, // Enable NAT port mapping via NAT-PMP (default=true). NodeJS only.
|
||||
webSeeds: Boolean, // Enable BEP19 web seeds (default=true)
|
||||
utp: Boolean, // Enable BEP29 uTorrent transport protocol (default=true)
|
||||
seedOutgoingConnections: Boolean // Enable outgoing connections when seeding (default=true)
|
||||
blocklist: Array|String, // List of IP's to block
|
||||
downloadLimit: Number, // Max download speed (bytes/sec) over all torrents (default=-1)
|
||||
uploadLimit: Number, // Max upload speed (bytes/sec) over all torrents (default=-1)
|
||||
}
|
||||
```
|
||||
|
||||
For possible values of `opts.dht` see the
|
||||
[`bittorrent-dht` documentation](https://github.com/webtorrent/bittorrent-dht#dht--new-dhtopts).
|
||||
|
||||
For possible values of `opts.tracker` see the
|
||||
[`bittorrent-tracker` documentation](https://github.com/webtorrent/bittorrent-tracker#client).
|
||||
|
||||
For possible values of `opts.blocklist` see the
|
||||
[`load-ip-set` documentation](https://github.com/webtorrent/load-ip-set#usage).
|
||||
|
||||
For `opts.natUpnp` and `opts.natPmp`, if both are set to `true`, PMP will be attempted first, then fallback to UPNP. NodeJS only.
|
||||
|
||||
For `opts.natUpnp`, if set to `true`, a temporary mapping is used, if set to `permanent`, a permanent TTL will be used for UPNP if the router only supports permanent leases. NodeJS only.
|
||||
|
||||
For `opts.seedOutgoingConnections`, if set `true`, outgoing connections will be established while seeding, otherwise, only inbound connections will be responded to.
|
||||
|
||||
For `downloadLimit` and `uploadLimit` the possible values can be:
|
||||
|
||||
- `> 0`. The client will set the throttle at that speed
|
||||
- `0`. The client will block any data from being downloaded or uploaded
|
||||
- `-1`. The client will is disable the throttling and use the whole bandwidth available
|
||||
|
||||
## `client.add(torrentId, [opts], [function ontorrent (torrent) {}])`
|
||||
|
||||
Start downloading a new torrent.
|
||||
|
||||
`torrentId` can be one of:
|
||||
|
||||
- magnet uri (string)
|
||||
- torrent file (Uint8Array)
|
||||
- info hash (hex string or Uint8Array)
|
||||
- parsed torrent (from [parse-torrent](https://github.com/webtorrent/parse-torrent))
|
||||
- http/https url to a torrent file (string)
|
||||
- filesystem path to a torrent file (string) _(Node.js only)_
|
||||
|
||||
If `opts` is specified, then the default options (shown below) will be overridden.
|
||||
|
||||
```js
|
||||
{
|
||||
announce: [String], // Torrent trackers to use (added to list in .torrent or magnet uri)
|
||||
getAnnounceOpts: Function, // Custom callback to allow sending extra parameters to the tracker
|
||||
urlList: [String], // Array of web seeds
|
||||
maxWebConns: Number, // Max number of simultaneous connections per web seed [default=4]
|
||||
path: String, // Folder to download files to (default=`/tmp/webtorrent/`)
|
||||
private: Boolean, // If true, client will not share the hash with the DHT nor with PEX (default is the privacy of the parsed torrent)
|
||||
store: Function, // Custom chunk store (must follow [abstract-chunk-store](https://www.npmjs.com/package/abstract-chunk-store) API)
|
||||
destroyStoreOnDestroy: Boolean, // If truthy, client will delete the torrent's chunk store (e.g. files on disk) when the torrent is destroyed
|
||||
storeCacheSlots: Number, // Number of chunk store entries (torrent pieces) to cache in memory [default=20]; 0 to disable caching
|
||||
storeOpts: Object, // Custom options passed to the store
|
||||
addUID: Boolean, // (Node.js only) If true, the torrent will be stored in it's infoHash folder to prevent file name collisions (default=false)
|
||||
skipVerify: Boolean, // If true, client will skip verification of pieces for existing store and assume it's correct
|
||||
preloadedStore: Function, // Custom, pre-loaded chunk store (must follow [abstract-chunk-store](https://www.npmjs.com/package/abstract-chunk-store) API)
|
||||
strategy: String, // Piece selection strategy, `rarest` or `sequential`(defaut=`sequential`)
|
||||
noPeersIntervalTime: Number, // The amount of time (in seconds) to wait between each check of the `noPeers` event (default=30)
|
||||
paused: Boolean, // If true, create the torrent in a paused state (default=false)
|
||||
deselect: Boolean, // If true, create the torrent with no pieces selected (default=false)
|
||||
alwaysChokeSeeders: Boolean // If true, client will automatically choke seeders if it's seeding. (default=true)
|
||||
}
|
||||
```
|
||||
|
||||
If `ontorrent` is specified, then it will be called when **this** torrent is ready to be
|
||||
used (i.e. metadata is available). Note: this is distinct from the 'torrent' event which
|
||||
will fire for **all** torrents.
|
||||
|
||||
If you want access to the torrent object immediately in order to listen to events as the
|
||||
metadata is fetched from the network, then use the return value of `client.add`. If you
|
||||
just want the file data, then use `ontorrent` or the 'torrent' event.
|
||||
|
||||
If you provide `opts.store`, it will be called as
|
||||
`opts.store(chunkLength, storeOpts)` with:
|
||||
|
||||
- `storeOpts` - custom `storeOpts` specified in `opts`
|
||||
- `storeOpts.length` - size of all the files in the torrent
|
||||
- `storeOpts.files` - an array of torrent file objects
|
||||
- `storeOpts.torrent` - the torrent instance being stored
|
||||
- `storeOpts.path` - path to the store, based on `opts.path`
|
||||
- `storeOpts.name` - the info hash of the torrent instance being stored
|
||||
- `storeOpts.addUID` - boolean which tells the store if it should include an UID in it's file paths
|
||||
- `storeOpts.rootDir` - _(browser only)_ [FileSystemDirectoryHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle) - if supported by the browser, allows the user to specify a custom directory to stores the files in, retaining the torrent's folder and file structure
|
||||
|
||||
**Note (browser only):** If you don't want to retain data across sessions, make sure to manually destroy the torrent store when the page closes (More on how below). This has to happen on the `beforeunload` event at latest, in order for the data to be removed. [About page lifecycles.](https://developers.google.com/web/updates/2018/07/page-lifecycle-api)
|
||||
|
||||
**Note:** Downloading a torrent automatically seeds it, making it available for download by other peers.
|
||||
|
||||
## `client.seed(input, [opts], [function onseed (torrent) {}])`
|
||||
|
||||
Start seeding a new torrent.
|
||||
|
||||
`input` can be any of the following:
|
||||
|
||||
- filesystem path to file or folder
|
||||
(string) _(Node.js only)_
|
||||
- W3C [FileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList) object (basically an array of `File` objects) _(browser only)_
|
||||
- W3C [File](https://developer.mozilla.org/en-US/docs/Web/API/File)/[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object (from an `<input>` or drag and drop)
|
||||
- typed array or array of numbers
|
||||
- Node [Buffer](https://nodejs.org/api/buffer.html) object
|
||||
- Node [Readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable) object
|
||||
|
||||
Or, an **array of of any of those values**.
|
||||
|
||||
If `opts` is specified, it should contain the following types of options:
|
||||
|
||||
- options for [create-torrent](https://github.com/webtorrent/create-torrent#createtorrentinput-opts-function-callback-err-torrent-) (to allow configuration of the .torrent file that is created)
|
||||
- options for `client.add` (see above)
|
||||
|
||||
If `onseed` is specified, it will be called when the client has begun seeding the file.
|
||||
|
||||
**Note:** Every torrent is required to have a name. If one is not explicitly provided
|
||||
through `opts.name`, one will be determined automatically using the following logic:
|
||||
|
||||
- If all files share a common path prefix, that will be used. For example, if all file
|
||||
paths start with `/imgs/` the torrent name will be `imgs`.
|
||||
- Otherwise, the first file that has a name will determine the torrent name. For example,
|
||||
if the first file is `/foo/bar/baz.txt`, the torrent name will be `baz.txt`.
|
||||
- If no files have names (say that all files are Uint8Array or Stream objects), then a name
|
||||
like "Unnamed Torrent <id>" will be generated.
|
||||
|
||||
**Note:** Every file is required to have a name. For filesystem paths or W3C File objects,
|
||||
the name is included in the object. For Uint8Array or Readable stream types, a `name` property
|
||||
can be set on the object, like this:
|
||||
|
||||
```js
|
||||
const buf = new Uint8Array("Some file content");
|
||||
buf.name = "Some file name";
|
||||
client.seed(buf, cb);
|
||||
```
|
||||
|
||||
## `client.on('add', function (torrent) {})`
|
||||
|
||||
Emitted when a torrent is added to client.torrents. This allows attaching to torrent events that may be emitted before the client 'torrent' event is emitted. See the torrent section for more info on what methods a `torrent` has.
|
||||
|
||||
## `client.on('remove', function (torrent) {})`
|
||||
|
||||
Emitted when a torrent is removed from client.torrents. See the torrent section for more info on what methods a `torrent` has.
|
||||
|
||||
## `client.on('torrent', function (torrent) {})`
|
||||
|
||||
Emitted when a torrent is ready to be used (i.e. metadata is available and store is
|
||||
ready). See the torrent section for more info on what methods a `torrent` has.
|
||||
|
||||
## `client.on('error', function (err) {})`
|
||||
|
||||
Emitted when the client encounters a fatal error. The client is automatically
|
||||
destroyed and all torrents are removed and cleaned up when this occurs.
|
||||
|
||||
Always listen for the 'error' event.
|
||||
|
||||
## `await client.remove(torrentId, [opts], [function callback (err) {}])`
|
||||
|
||||
Remove a torrent from the client. Destroy all connections to peers and delete all saved file metadata.
|
||||
|
||||
If `opts.destroyStore` is specified, it will override `opts.destroyStoreOnDestroy` passed when the torrent was added.
|
||||
If truthy, `store.destroy()` will be called, which will delete the torrent's files from the disk.
|
||||
|
||||
If `callback` is provided, it will be called when the torrent is fully destroyed,
|
||||
i.e. all open sockets are closed, and the storage is either closed or destroyed.
|
||||
|
||||
## `client.destroy([function callback (err) {}])`
|
||||
|
||||
Destroy the client, including all torrents and connections to peers. If `callback` is specified, it will be called when the client has gracefully closed.
|
||||
|
||||
## `client.torrents[...]`
|
||||
|
||||
An array of all torrents in the client.
|
||||
|
||||
## `await client.get(torrentId)`
|
||||
|
||||
Returns a promise which resolves the torrent with the given `torrentId`. Convenience method. Easier than searching
|
||||
through the `client.torrents` array. Returns `null` if no matching torrent found.
|
||||
|
||||
## `client.downloadSpeed`
|
||||
|
||||
Total download speed for all torrents, in bytes/sec.
|
||||
|
||||
## `client.uploadSpeed`
|
||||
|
||||
Total upload speed for all torrents, in bytes/sec.
|
||||
|
||||
## `client.progress`
|
||||
|
||||
Total download progress for all **active** torrents, from 0 to 1.
|
||||
|
||||
## `client.ratio`
|
||||
|
||||
Aggregate "seed ratio" for all torrents (uploaded / downloaded).
|
||||
|
||||
## `client.throttleDownload(rate)`
|
||||
|
||||
Sets the maximum speed at which the client downloads the torrents, in bytes/sec.
|
||||
|
||||
`rate` must be bigger or equal than zero, or `-1` to disable the download throttle and
|
||||
use the whole bandwidth of the connection.
|
||||
|
||||
## `client.throttleUpload(rate)`
|
||||
|
||||
Sets the maximum speed at which the client uploads the torrents, in bytes/sec.
|
||||
|
||||
`rate` must be bigger or equal than zero, or `-1` to disable the upload throttle and
|
||||
use the whole bandwidth of the connection.
|
||||
|
||||
## `client.createServer([opts], force)`
|
||||
|
||||
Create an http server to serve the contents of this torrent, dynamically fetching the needed torrent pieces to satisfy http requests. Range requests are supported.
|
||||
If `opts` is specified, it can have the following properties:
|
||||
|
||||
```js
|
||||
{
|
||||
origin: String; // Allow requests from specific origin. `false` for same-origin. [default: '*']
|
||||
hostname: String; // If specified, only allow requests whose `Host` header matches this hostname. Note that you should not specify the port since this is automatically determined by the server. Ex: `localhost` [default: `undefined`]. NodeJS only.
|
||||
path: String; // Allows to overwrite the default `/webtorrent` base path. [default: '/webtorrent']. NodeJS only.
|
||||
controller: ServiceWorkerRegistration; // Accepts an existing service worker registration [await navigator.serviceWorker.getRegistration()]. Browser only. Required!
|
||||
}
|
||||
```
|
||||
|
||||
If `force` is specified, it can force WebTorrent to use a specific implementation for enviorments which run both Node and Browser like NW.js or Electron. Allowed values:
|
||||
|
||||
```js
|
||||
"browser" || "node";
|
||||
```
|
||||
|
||||
Visiting the root of the server `/` won't show anything. Visiting `/webtorrent/` will list all torrents. Access individual torrents at `/webtorrent/<infohash>` where `infohash` is the hash of the torrent. To acceess individual files, go to `/webtorrent/<infoHash>/<filepath>` where filepath is the file's path in the torrent.
|
||||
|
||||
Here is a usage example for Node.js:
|
||||
|
||||
```js
|
||||
const client = new WebTorrent();
|
||||
const magnetURI = "magnet: ...";
|
||||
|
||||
const instance = client.createServer();
|
||||
instance.server.listen(0); // start the server listening to a port
|
||||
// 0 automatically finds an open port instead of forcing a potentially used one
|
||||
client.add(magnetURI, (torrent) => {
|
||||
// create HTTP server for this torrent
|
||||
|
||||
const url = torrent.files[0].streamURL;
|
||||
console.log(url);
|
||||
// visit http://localhost:<port>/webtorrent/ to see a list of torrents
|
||||
|
||||
// access individual torrents at http://localhost:<port>/webtorrent/<infoHash> where infoHash is the hash of the torrent
|
||||
});
|
||||
|
||||
// later, cleanup...
|
||||
instance.close();
|
||||
client.destroy();
|
||||
```
|
||||
|
||||
In browser needs either [this worker](https://github.com/webtorrent/webtorrent/blob/master/sw.min.js) to be used, or have [this functionality](https://github.com/webtorrent/webtorrent/blob/master/lib/worker.js) implemented.
|
||||
|
||||
Here is a user example for browser:
|
||||
|
||||
```js
|
||||
const client = new WebTorrent();
|
||||
const magnetURI = "magnet: ...";
|
||||
const player = document.querySelector("video");
|
||||
|
||||
function download(instance) {
|
||||
client.add(magnetURI, (torrent) => {
|
||||
const url = torrent.files[0].streamURL;
|
||||
console.log(url);
|
||||
// visit <origin>/webtorrent/ to see a list of torrents, where origin is the worker registration scope.
|
||||
|
||||
// access individual torrents at /webtorrent/<infoHash> where infoHash is the hash of the torrent
|
||||
});
|
||||
}
|
||||
navigator.serviceWorker.register("./sw.min.js", { scope: "./" }).then((reg) => {
|
||||
const worker = reg.active || reg.waiting || reg.installing;
|
||||
function checkState(worker) {
|
||||
return (
|
||||
worker.state === "activated" &&
|
||||
download(client.createServer({ controller: reg }))
|
||||
);
|
||||
}
|
||||
if (!checkState(worker)) {
|
||||
worker.addEventListener("statechange", ({ target }) => checkState(target));
|
||||
}
|
||||
});
|
||||
|
||||
// later, cleanup...
|
||||
client._server.close();
|
||||
client.destroy();
|
||||
```
|
||||
|
||||
Needs either [this worker](https://github.com/webtorrent/webtorrent/blob/master/sw.min.js) to be used, or have [this functionality](https://github.com/webtorrent/webtorrent/blob/master/lib/worker.js) implemented.
|
||||
|
||||
# Torrent API
|
||||
|
||||
## `torrent.name`
|
||||
|
||||
Name of the torrent (string).
|
||||
|
||||
## `torrent.infoHash`
|
||||
|
||||
Info hash of the torrent (string).
|
||||
|
||||
## `torrent.magnetURI`
|
||||
|
||||
Magnet URI of the torrent (string).
|
||||
|
||||
## `torrent.torrentFile`
|
||||
|
||||
`.torrent` file of the torrent (Uint8Array).
|
||||
|
||||
## `torrent.torrentFileBlob`
|
||||
|
||||
`.torrent` file of the torrent (Blob). Useful for creating Blob URLs via `URL.createObjectURL(blob)`
|
||||
|
||||
## `torrent.announce[...]`
|
||||
|
||||
Array of all tracker servers. Each announce is an URL (string).
|
||||
|
||||
## `torrent.files[...]`
|
||||
|
||||
Array of all files in the torrent. See documentation for `File` below to learn what
|
||||
methods/properties files have.
|
||||
|
||||
## `torrent.pieces[...]`
|
||||
|
||||
Array of all pieces in the torrent. See documentation for `Piece` below to learn what
|
||||
properties pieces have. Some pieces can be null.
|
||||
|
||||
## `torrent.pieceLength`
|
||||
|
||||
Length in bytes of every piece but the last one.
|
||||
|
||||
## `torrent.lastPieceLength`
|
||||
|
||||
Length in bytes of the last piece (<= of `torrent.pieceLength`).
|
||||
|
||||
## `torrent.timeRemaining`
|
||||
|
||||
Time remaining for download to complete (in milliseconds).
|
||||
|
||||
## `torrent.received`
|
||||
|
||||
Total bytes received from peers (_including_ invalid data).
|
||||
|
||||
## `torrent.downloaded`
|
||||
|
||||
Total _verified_ bytes received from peers.
|
||||
|
||||
## `torrent.uploaded`
|
||||
|
||||
Total bytes uploaded to peers.
|
||||
|
||||
## `torrent.downloadSpeed`
|
||||
|
||||
Torrent download speed, in bytes/sec.
|
||||
|
||||
## `torrent.uploadSpeed`
|
||||
|
||||
Torrent upload speed, in bytes/sec.
|
||||
|
||||
## `torrent.progress`
|
||||
|
||||
Torrent download progress, from 0 to 1.
|
||||
|
||||
## `torrent.ratio`
|
||||
|
||||
Torrent "seed ratio" (uploaded / downloaded).
|
||||
|
||||
## `torrent.numPeers`
|
||||
|
||||
Number of peers in the torrent swarm.
|
||||
|
||||
## `torrent.maxWebConns`
|
||||
|
||||
Max number of simultaneous connections per web seed, as passed in the options.
|
||||
|
||||
## `torrent.path`
|
||||
|
||||
Torrent download location.
|
||||
|
||||
## `torrent.ready`
|
||||
|
||||
True when the torrent is ready to be used (i.e. metadata is available and store is
|
||||
ready).
|
||||
|
||||
## `torrent.paused`
|
||||
|
||||
True when the torrent has stopped connecting to new peers. Note that this does
|
||||
not pause new incoming connections, nor does it pause the streams of existing
|
||||
connections or their wires.
|
||||
|
||||
## `torrent.done`
|
||||
|
||||
True when all the torrent files have been downloaded.
|
||||
|
||||
## `torrent.length`
|
||||
|
||||
Sum of the files length (in bytes).
|
||||
|
||||
## `torrent.created`
|
||||
|
||||
Date of creation of the torrent (as a [Date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object).
|
||||
|
||||
## `torrent.createdBy`
|
||||
|
||||
Author of the torrent (string).
|
||||
|
||||
## `torrent.comment`
|
||||
|
||||
A comment optionnaly set by the author (string).
|
||||
|
||||
## `torrent.destroy([opts], [callback])`
|
||||
|
||||
Remove the torrent from its client. Destroy all connections to peers and delete all saved file metadata.
|
||||
|
||||
If `opts.destroyStore` is specified, it will override `opts.destroyStoreOnDestroy` passed when the torrent was added.
|
||||
If truthy, `store.destroy()` will be called, which will delete the torrent's files from the disk.
|
||||
|
||||
If `callback` is provided, it will be called when the torrent is fully destroyed,
|
||||
i.e. all open sockets are closed, and the storage is either closed or destroyed.
|
||||
|
||||
## `torrent.addPeer(peer)`
|
||||
|
||||
Add a peer to the torrent swarm. This is advanced functionality. Normally, you should not
|
||||
need to call `torrent.addPeer()` manually. WebTorrent will automatically find peers using
|
||||
the tracker servers or DHT. This is just for manually adding a peer to the client.
|
||||
|
||||
This method should not be called until the `infoHash` event has been emitted.
|
||||
|
||||
Returns `true` if peer was added, `false` if peer was blocked by the loaded blocklist.
|
||||
|
||||
The `peer` argument must be an address string in the format `12.34.56.78:4444` (for
|
||||
normal TCP/uTP peers), or a [`simple-peer`](https://github.com/feross/simple-peer)
|
||||
instance (for WebRTC peers).
|
||||
|
||||
## `torrent.addWebSeed(urlOrConn)`
|
||||
|
||||
Add a web seed to the torrent swarm. For more information on BitTorrent web seeds, see
|
||||
[BEP19](http://www.bittorrent.org/beps/bep_0019.html).
|
||||
|
||||
In the browser, web seed servers must have proper CORS (Cross-origin resource sharing)
|
||||
headers so that data can be fetched across domain.
|
||||
|
||||
The `urlOrConn` argument is either the web seed URL, or an object that provides a custom
|
||||
web seed implementation. A custom conn object is a duplex stream that speaks the bittorrent
|
||||
wire protocol and pretends to be a remote peer. It must have a `connId` property that
|
||||
uniquely identifies the custom web seed.
|
||||
|
||||
## `torrent.removePeer(peer)`
|
||||
|
||||
Remove a peer from the torrent swarm. This is advanced functionality. Normally, you should
|
||||
not need to call `torrent.removePeer()` manually. WebTorrent will automatically remove
|
||||
peers from the torrent swarm when they're slow or don't have pieces that are needed.
|
||||
|
||||
The `peer` argument should be an address (i.e. "ip:port" string), a peer id (hex string),
|
||||
or `simple-peer` instance.
|
||||
|
||||
## `torrent.select(start, end, [priority], [notify])`
|
||||
|
||||
Selects a range of pieces to prioritize starting with `start` and ending with `end` (both
|
||||
inclusive) at the given `priority`. `notify` is an optional callback to be called when the
|
||||
selection is updated with new data.
|
||||
|
||||
## `torrent.deselect(start, end)`
|
||||
|
||||
Deprioritizes a range of previously selected pieces.
|
||||
|
||||
## `torrent.critical(start, end)`
|
||||
|
||||
Marks a range of pieces as critical priority to be downloaded ASAP. From `start` to `end`
|
||||
(both inclusive).
|
||||
|
||||
## `torrent.pause()`
|
||||
|
||||
Temporarily stop connecting to new peers. Note that this does not pause new incoming
|
||||
connections, nor does it pause the streams of existing connections or their wires.
|
||||
|
||||
## `torrent.resume()`
|
||||
|
||||
Resume connecting to new peers.
|
||||
|
||||
## `torrent.rescanFiles([function callback (err) {}])`
|
||||
|
||||
Verify the hashes of all pieces in the store and update the bitfield for any new valid
|
||||
pieces. Useful if data has been added to the store outside WebTorrent, e.g. if another
|
||||
process puts a valid file in the right place. Once the scan is complete,
|
||||
`callback(null)` will be called (if provided), unless the torrent was destroyed during
|
||||
the scan, in which case `callback` will be called with an error.
|
||||
|
||||
## `torrent.on('infoHash', function () {})`
|
||||
|
||||
Emitted when the info hash of the torrent has been determined.
|
||||
|
||||
## `torrent.on('metadata', function () {})`
|
||||
|
||||
Emitted when the metadata of the torrent has been determined. This includes the full
|
||||
contents of the .torrent file, including list of files, torrent length, piece hashes,
|
||||
piece length, etc.
|
||||
|
||||
## `torrent.on('ready', function () {})`
|
||||
|
||||
Emitted when the torrent is ready to be used (i.e. metadata is available and store is
|
||||
ready).
|
||||
|
||||
## `torrent.on('warning', function (err) {})`
|
||||
|
||||
Emitted when there is a warning. This is purely informational and it is not necessary to
|
||||
listen to this event, but it may aid in debugging.
|
||||
|
||||
## `torrent.on('error', function (err) {})`
|
||||
|
||||
Emitted when the torrent encounters a fatal error. The torrent is automatically destroyed
|
||||
and removed from the client when this occurs.
|
||||
|
||||
**Note:** Torrent errors are emitted at `torrent.on('error')`. If there are no
|
||||
'error' event handlers on the torrent instance, then the error will be emitted at
|
||||
`client.on('error')`. This prevents throwing an uncaught exception (unhandled
|
||||
'error' event), but it makes it impossible to distinguish client errors versus
|
||||
torrent errors. Torrent errors are not fatal, and the client is still usable
|
||||
afterwards. Therefore, always listen for errors in both places
|
||||
(`client.on('error')` and `torrent.on('error')`).
|
||||
|
||||
## `torrent.on('done', function () {})`
|
||||
|
||||
Emitted when all the torrent files have been downloaded.
|
||||
|
||||
Here is a usage example:
|
||||
|
||||
```js
|
||||
torrent.on("done", () => {
|
||||
console.log("torrent finished downloading");
|
||||
for (const file of torrent.files) {
|
||||
// do something with file
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## `torrent.on('download', function (bytes) {})`
|
||||
|
||||
Emitted whenever data is downloaded. Useful for reporting the current torrent status, for
|
||||
instance:
|
||||
|
||||
```js
|
||||
torrent.on("download", (bytes) => {
|
||||
console.log("just downloaded: " + bytes);
|
||||
console.log("total downloaded: " + torrent.downloaded);
|
||||
console.log("download speed: " + torrent.downloadSpeed);
|
||||
console.log("progress: " + torrent.progress);
|
||||
});
|
||||
```
|
||||
|
||||
## `torrent.on('upload', function (bytes) {})`
|
||||
|
||||
Emitted whenever data is uploaded. Useful for reporting the current torrent status.
|
||||
|
||||
## `torrent.on('wire', function (wire) {})`
|
||||
|
||||
Emitted whenever a new peer is connected for this torrent. `wire` is an instance of
|
||||
[`bittorrent-protocol`](https://github.com/webtorrent/bittorrent-protocol), which is a
|
||||
node.js-style duplex stream to the remote peer. This event can be used to specify
|
||||
[custom BitTorrent protocol extensions](https://github.com/webtorrent/bittorrent-protocol#extension-api).
|
||||
|
||||
Here is a usage example:
|
||||
|
||||
```js
|
||||
import MyExtension from "./my-extension";
|
||||
|
||||
torrent1.on("wire", (wire, addr) => {
|
||||
console.log("connected to peer with address " + addr);
|
||||
wire.use(MyExtension);
|
||||
});
|
||||
```
|
||||
|
||||
See the `bittorrent-protocol`
|
||||
[extension api docs](https://github.com/webtorrent/bittorrent-protocol#extension-api) for more
|
||||
information on how to define a protocol extension.
|
||||
|
||||
## `torrent.on('noPeers', function (announceType) {})`
|
||||
|
||||
Emitted every couple of seconds when no peers have been found. `announceType` is either `'tracker'`, `'dht'`, `'lsd'`, or `'ut_pex'` depending on which announce occurred to trigger this event. Note that if you're attempting to discover peers from a tracker, a DHT, a LSD, and PEX you'll see this event separately for each.
|
||||
|
||||
## `torrent.on('verified', function (index) {})`
|
||||
|
||||
Emitted every time a piece is verified, the value of the event is the index of the verified piece.
|
||||
|
||||
# File API
|
||||
|
||||
Webtorrent Files closely mimic W3C [Files](https://developer.mozilla.org/en-US/docs/Web/API/File)/[Blobs](https://developer.mozilla.org/en-US/docs/Web/API/Blob) except for `slice` where instead you pass the offsets as objects to the arrayBuffer/stream/createReadStream functions.
|
||||
|
||||
## `file.name`
|
||||
|
||||
File name, as specified by the torrent. _Example: 'some-filename.txt'_
|
||||
|
||||
## `file.path`
|
||||
|
||||
File path, as specified by the torrent. _Example: 'some-folder/some-filename.txt'_
|
||||
|
||||
## `file.length` or `file.size`
|
||||
|
||||
File length (in bytes), as specified by the torrent. _Example: 12345_
|
||||
|
||||
## `file.type`
|
||||
|
||||
Mime type of the file, falls back to `application/octet-stream` if the type is not recognized.
|
||||
|
||||
## `file.downloaded`
|
||||
|
||||
Total _verified_ bytes received from peers, for this file.
|
||||
|
||||
## `file.progress`
|
||||
|
||||
File download progress, from 0 to 1.
|
||||
|
||||
## `file.select([priority])`
|
||||
|
||||
Selects the file to be downloaded, at the given `priority`.
|
||||
Useful if you know you need the file at a later stage.
|
||||
|
||||
## `file.deselect()`
|
||||
|
||||
Deselects the file's specific priority, which means it won't be downloaded unless someone creates a stream for it.
|
||||
|
||||
\*Note: This method is currently not working as expected, see [dcposch answer on #164](https://github.com/webtorrent/webtorrent/issues/164) for a nice work around solution.
|
||||
|
||||
## `stream = file.createReadStream([opts])`
|
||||
|
||||
Create a [readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable)
|
||||
to the file. Pieces needed by the stream will be prioritized highly and fetched from the
|
||||
swarm first.
|
||||
|
||||
You can pass `opts` to stream only a slice of a file.
|
||||
|
||||
```js
|
||||
{
|
||||
start: startByte,
|
||||
end: endByte
|
||||
}
|
||||
```
|
||||
|
||||
Both `start` and `end` are inclusive.
|
||||
|
||||
## `stream = file.stream(opts)`
|
||||
|
||||
Create a W3C [ReadableStream](https://devdocs.io/dom/readablestream)
|
||||
to the file. Pieces needed by the stream will be prioritized highly and fetched from the
|
||||
swarm first.
|
||||
|
||||
You can pass `opts` to stream only a slice of a file.
|
||||
|
||||
```js
|
||||
{
|
||||
start: startByte,
|
||||
end: endByte
|
||||
}
|
||||
```
|
||||
|
||||
Both `start` and `end` are inclusive.
|
||||
|
||||
## `iterator = file[Symbol.asyncIterator]`
|
||||
|
||||
Create an [async iterator](https://devdocs.io/javascript/global_objects/symbol/asynciterator)
|
||||
to the file. Pieces needed by the stream will be prioritized highly and fetched from the
|
||||
swarm first.
|
||||
|
||||
You can pass `opts` to iterate only a slice of a file.
|
||||
|
||||
```js
|
||||
{
|
||||
start: startByte,
|
||||
end: endByte
|
||||
}
|
||||
```
|
||||
|
||||
Both `start` and `end` are inclusive.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
for await (const chunk of file) {
|
||||
// do something with chunk
|
||||
}
|
||||
```
|
||||
|
||||
## `arrayBuffer = await file.arrayBuffer(opts)`
|
||||
|
||||
Get the file contents as a `ArrayBuffer`.
|
||||
|
||||
You can pass `opts` to get only a part of an ArrayBuffer.
|
||||
|
||||
```js
|
||||
{
|
||||
start: startByte,
|
||||
end: endByte
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
const data = await file.arrayBuffer();
|
||||
console.log(data); // ArrayBuffer { [Uint8Contents]: <00 62 00 01>, byteLength: 4 }
|
||||
```
|
||||
|
||||
## `blob = await file.blob(opts)`
|
||||
|
||||
Get a W3C `Blob` object which contains the file data.
|
||||
|
||||
Useful for creating Blob URLs via `URL.createObjectURL(blob)`.
|
||||
|
||||
You can pass `opts` to get only a part of an Blob.
|
||||
|
||||
```js
|
||||
{
|
||||
start: startByte,
|
||||
end: endByte
|
||||
}
|
||||
```
|
||||
|
||||
## `file.streamTo(elem)` _(browser only)_
|
||||
|
||||
Requires `client.createServer` to be ran beforehand. Sets the element source to the file's streaming URL. Supports streaming, seeking and all browser codecs and containers.
|
||||
|
||||
Support table:
|
||||
|Containers|Chromium|Mobile Chromium|Edge|Chrome|Firefox|
|
||||
|-|:-:|:-:|:-:|:-:|:-:|
|
||||
|3g2|✓|✓|✓|✓|✓|
|
||||
|3gp|✓|✓|✓|✓|✘|
|
||||
|avi|✘|✘|✘|✘|✘|
|
||||
|m2ts|✘|✘|✓\*_|✘|✘|
|
||||
|m4v etc.|✓_|✓*|✓*|✓*|✓*|
|
||||
|mp4|✓|✓|✓|✓|✓|
|
||||
|mpeg|✘|✘|✘|✘|✘|
|
||||
|mov|✓|✓|✓|✓|✓|
|
||||
|ogm ogv|✓|✓|✓|✓|✓|
|
||||
|webm|✓|✓|✓|✓|✓|
|
||||
|mkv|✓|✓|✓|✓|✘|
|
||||
|
||||
\* Container might be supported, but the container's codecs might not be.
|
||||
\*\* Documented as working, but can't reproduce.
|
||||
|
||||
| Video Codecs | Chromium | Mobile Chromium | Edge | Chrome | Firefox |
|
||||
| ------------ | :------: | :-------------: | :--: | :----: | :-----: |
|
||||
| AV1 | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| H.263 | ✘ | ✘ | ✘ | ✘ | ✘ |
|
||||
| H.264 | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| H.265 | ✘ | ✘ | ✓\* | ✓ | ✘ |
|
||||
| MPEG-2/4 | ✘ | ✘ | ✘ | ✘ | ✘ |
|
||||
| Theora | ✓ | ✘ | ✓ | ✓ | ✓ |
|
||||
| VP8/9 | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
|
||||
\* Requires MSStore extension which you can get by opening this link `ms-windows-store://pdp/?ProductId=9n4wgh0z6vhq` while using Edge.
|
||||
|
||||
| Audio Codecs | Chromium | Mobile Chromium | Edge | Chrome | Firefox |
|
||||
| ------------ | :------: | :-------------: | :--: | :----: | :-----: |
|
||||
| AAC | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| AC3 | ✘ | ✘ | ✓ | ✘ | ✘ |
|
||||
| DTS | ✘ | ✘ | ✘ | ✘ | ✘ |
|
||||
| EAC3 | ✘ | ✘ | ✓ | ✘ | ✘ |
|
||||
| FLAC | ✓ | ✓\* | ✓ | ✓ | ✓ |
|
||||
| MP3 | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Opus | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| TrueHD | ✘ | ✘ | ✘ | ✘ | ✘ |
|
||||
| Vorbis | ✓ | ✓ | ✓ | ✓ | ✓\* |
|
||||
|
||||
\* Might not work in some video containers.
|
||||
|
||||
Since container and codec support is browser dependent these values might change over time.
|
||||
|
||||
## `file.streamURL`
|
||||
|
||||
Requires `client.createServer` to be ran beforehand.
|
||||
|
||||
Returns the URL of the file which is recognized by the HTTP server.
|
||||
|
||||
This method is useful both for servers which run WebTorrent or client apps. A few examples:
|
||||
|
||||
```js
|
||||
const url = file.streamURL;
|
||||
|
||||
// create download link
|
||||
if (err) throw err;
|
||||
const a = document.createElement("a");
|
||||
a.target = "_blank";
|
||||
a.href = url;
|
||||
a.textContent = "Download " + file.name;
|
||||
document.body.append(a);
|
||||
|
||||
// render an image on a canvas
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
const loaded = new Promise((resolve) => (img.onload = resolve));
|
||||
img.src = url;
|
||||
await loaded;
|
||||
ctx.drawImage(img);
|
||||
|
||||
// send the file URL to another device on the network which can then display the file remotely [nodejs only]
|
||||
import networkAddress from "network-address";
|
||||
|
||||
const networkURL = `http://${networkAddress()}:${client._server.port}${url}`;
|
||||
sendRemote(networkURL);
|
||||
```
|
||||
|
||||
## `file.on('stream', function ({ stream, file, req }, function pipeCallback) {})`
|
||||
|
||||
This is advanced functionality.
|
||||
|
||||
Emitted every time when the HTTP server creates a new read stream. For example every time the user seeks a video. This allows you to find out what parts of the file the browser is requesting, and how it's requesting them. Additionally it allows you to manipulate the data that's being streamed.
|
||||
|
||||
Yields an object with 3 values and a function:
|
||||
|
||||
- object - information about the request,
|
||||
- `stream` - a [readable stream](https://nodejs.org/api/stream.html#stream_class_stream_readable) which the user can manipulate,
|
||||
- `file` - the file object that's being streamed,
|
||||
- `req` - all the request information which the browser made when requesting the data.
|
||||
- function - if you pipe the `stream`, use this function to callback the piped stream **synchronously!** Otherwise the playback is likely to break.
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
file.on("stream", ({ stream, file, req }, cb) => {
|
||||
if (req.destination === "audio" && file.name.endsWith(".dts")) {
|
||||
const transcoder = new SomeAudioTranscoder();
|
||||
cb(transcoder);
|
||||
// do other things
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## `file.on('iterator', function ({ stream, file, req }, function transformCallback) {})`
|
||||
|
||||
This is advanced functionality.
|
||||
|
||||
Same as with the `stream` event this is emitted by the HTTP server when it creates an async iterator for the file's data. This is used for very low-level manipulation of the incoming data and they way it's generated for example you could potentially accelerate how fast and how much data is pulled from the torrent.
|
||||
|
||||
Yields an object with 3 values and a function:
|
||||
|
||||
- object - information about the request,
|
||||
- `iterator` - an [async iterator](https://devdocs.io/javascript/global_objects/symbol/asynciterator) which the user can manipulate,
|
||||
- `file` - the file object that's being streamed,
|
||||
- `req` - all the request information which the browser made when requesting the data.
|
||||
- function - if you wish to transform the `iterator`, use this function to callback the transformed iterator **synchronously!** Otherwise the playback is likely to break.
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
import par from "it-parallel";
|
||||
|
||||
file.on("iterator", ({ iterator, file, req }, cb) => {
|
||||
const transform = par(iterator, { concurrency: 5, ordered: true });
|
||||
cb(transform);
|
||||
});
|
||||
```
|
||||
|
||||
## `file.includes(piece)`
|
||||
|
||||
Check if the piece number contains this file's data.
|
||||
|
||||
## `file.on('done', function () {})`
|
||||
|
||||
Emitted when the file has been downloaded.
|
||||
|
||||
# Piece API
|
||||
|
||||
## `piece.length`
|
||||
|
||||
Piece length (in bytes). _Example: 12345_
|
||||
|
||||
## `piece.missing`
|
||||
|
||||
Piece missing length (in bytes). _Example: 100_
|
||||
|
||||
# Wire API
|
||||
|
||||
## `wire.peerId`
|
||||
|
||||
Remote peer id (hex string)
|
||||
|
||||
## `wire.type`
|
||||
|
||||
Connection type ('webrtc', 'tcpIncoming', 'tcpOutgoing', 'utpIncoming', 'utpOutgoing', 'webSeed')
|
||||
|
||||
## `wire.uploaded`
|
||||
|
||||
Total bytes uploaded to peer.
|
||||
|
||||
## `wire.downloaded`
|
||||
|
||||
Total bytes downloaded from peer.
|
||||
|
||||
## `wire.uploadSpeed`
|
||||
|
||||
Peer upload speed, in bytes/sec.
|
||||
|
||||
## `wire.downloadSpeed`
|
||||
|
||||
Peer download speed, in bytes/sec.
|
||||
|
||||
## `wire.remoteAddress`
|
||||
|
||||
Peer's remote address. Only exists for tcp/utp peers.
|
||||
|
||||
## `wire.remotePort`
|
||||
|
||||
Peer's remote port. Only exists for tcp/utp peers.
|
||||
|
||||
## `wire.destroy()`
|
||||
|
||||
Close the connection with the peer. This however doesn't prevent the peer from simply re-connecting.
|
@@ -1,29 +0,0 @@
|
||||
# BEP Support
|
||||
|
||||
These are the [BitTorrent Extension Protocols (BEP)](https://www.bittorrent.org/beps/bep_0001.html) supported by Webtorrent:
|
||||
|
||||
:white_check_mark: = implemented
|
||||
|
||||
:heavy_minus_sign: = not implemented (feel free to open an issue)
|
||||
|
||||
:x: = cannot be implemented
|
||||
|
||||
| Name | Link | Node.js | Browser |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| Distributed hash table (DHT) | [BEP 5](https://www.bittorrent.org/beps/bep_0005.html) | :white_check_mark: | :heavy_minus_sign: |
|
||||
| Fast Extension | [BEP 6](https://www.bittorrent.org/beps/bep_0006.html) | :white_check_mark: | :white_check_mark: |
|
||||
| IPv6 Tracker Extension | [BEP 7](https://www.bittorrent.org/beps/bep_0007.html) | :heavy_minus_sign: | :heavy_minus_sign: |
|
||||
| Magnet links | [BEP 9](https://www.bittorrent.org/beps/bep_0009.html) | :white_check_mark: | :white_check_mark: |
|
||||
| Extension Protocol | [BEP 10](https://www.bittorrent.org/beps/bep_0010.html) | :white_check_mark: | :white_check_mark: |
|
||||
| Peer Exchange (PEX) | [BEP 11](https://www.bittorrent.org/beps/bep_0011.html) | :white_check_mark: | :heavy_minus_sign: [webtorrent#1191](https://github.com/webtorrent/webtorrent/issues/1191) |
|
||||
| Local Service Discovery (LSD) | [BEP 14](https://www.bittorrent.org/beps/bep_0014.html) | :white_check_mark: | :x: |
|
||||
| UDP Tracker Protocol | [BEP 15](https://www.bittorrent.org/beps/bep_0015.html) | :white_check_mark: | :x: |
|
||||
| WebSeed - HTTP/FTP Seeding (GetRight style) | [BEP 19](https://www.bittorrent.org/beps/bep_0019.html) | :white_check_mark: | :white_check_mark: |
|
||||
| Tracker Returns Compact Peer Lists | [BEP 23](https://www.bittorrent.org/beps/bep_0023.html) | :white_check_mark: | :heavy_minus_sign: |
|
||||
| Private Torrents | [BEP 27](https://www.bittorrent.org/beps/bep_0027.html) | :white_check_mark: | :white_check_mark: |
|
||||
| uTorrent transport protocol (uTP) | [BEP 29](https://www.bittorrent.org/beps/bep_0029.html) | :white_check_mark: | :x: |
|
||||
| DHT Extensions for IPv6 | [BEP 32](https://www.bittorrent.org/beps/bep_0032.html) | :heavy_minus_sign: [bittorrent-dht#88](https://github.com/webtorrent/bittorrent-dht/issues/88) | :heavy_minus_sign: |
|
||||
| Updating Torrents Via DHT Mutable Items | [BEP 46](https://www.bittorrent.org/beps/bep_0046.html) | :heavy_minus_sign: [webtorrent#886](https://github.com/webtorrent/webtorrent/issues/886) | :heavy_minus_sign: [webtorrent#886](https://github.com/webtorrent/webtorrent/issues/886) |
|
||||
| Tracker Protocol Extension: Scrape | [BEP 48](https://www.bittorrent.org/beps/bep_0048.html) | :white_check_mark: | :white_check_mark: |
|
||||
| Magnet URI extension - Select specific file indices for download | [BEP 53](https://www.bittorrent.org/beps/bep_0053.html) | :white_check_mark: | :white_check_mark: |
|
||||
| BitTorrent Protocol v2 | [BEP 52](https://www.bittorrent.org/beps/bep_0052.html) | :heavy_minus_sign: [webtorrent#1117](https://github.com/webtorrent/webtorrent/issues/1117) | :heavy_minus_sign: [webtorrent#1117](https://github.com/webtorrent/webtorrent/issues/1117) |
|
@@ -1,587 +0,0 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
## What is WebTorrent?
|
||||
|
||||
**WebTorrent** is the first torrent client that works in the **browser**. YEP,
|
||||
THAT'S RIGHT. THE BROWSER.
|
||||
|
||||
It's written completely in JavaScript – the language of the web – and uses
|
||||
**WebRTC** for true peer-to-peer transport. No browser plugin, extension, or
|
||||
installation is required.
|
||||
|
||||
Using open web standards, WebTorrent connects website users together to form a
|
||||
distributed, decentralized browser-to-browser network for efficient file transfer.
|
||||
|
||||
## Why is this cool?
|
||||
|
||||
Imagine a video site like YouTube, where **visitors help to host the site's
|
||||
content**. The more people that use a WebTorrent-powered website, the faster and
|
||||
more resilient it becomes.
|
||||
|
||||
Browser-to-browser communication **cuts out the middle-man** and lets people
|
||||
communicate on their own terms. No more client/server – just a network of peers,
|
||||
all equal. WebTorrent is the first step in the journey to
|
||||
[redecentralize][redecentralize] the Web.
|
||||
|
||||
> The way we code the Web will determine the way we live online. So we need to bake
|
||||
> our values into our code. Freedom of expression needs to be baked into our code.
|
||||
> Privacy should be baked into our code. Universal access to all knowledge. But
|
||||
> right now, those values are not embedded in the Web.
|
||||
>
|
||||
> <cite>— Brewster Kahle, Founder of the Internet Archive (from [Locking the Web Open][brewster])
|
||||
|
||||
## What are some use cases for WebTorrent?
|
||||
|
||||
One of the most exciting uses for WebTorrent is **peer-assisted delivery**.
|
||||
Non-profit projects like [Wikipedia][wikipedia] and the [Internet
|
||||
Archive][archive] could reduce bandwidth and hosting costs by letting visitors
|
||||
chip in. Popular content is served browser-to-browser, quickly and cheaply.
|
||||
Rarely-accessed content is served reliably over HTTP from the origin server.
|
||||
|
||||
There are also exciting **business use cases**, from CDNs to app delivery.
|
||||
|
||||
> WebTorrent has significant business potential to radically change the traditional
|
||||
> notion of client-server, with applications for internal infrastructure and
|
||||
> external closed user communications. WebTorrent has moved from an “idea” to a
|
||||
> science experiment to now on the edge of being viable. This is like really,
|
||||
> seriously cool.
|
||||
>
|
||||
> <cite>— Chris Kranky (from ["WebTorrent: Rethinking Delivery"][kranky-article])</cite>
|
||||
|
||||
[wikipedia]: https://www.wikipedia.org/
|
||||
[archive]: https://archive.org/index.php
|
||||
[kranky-article]: https://www.chriskranky.com/webtorrent-rethinking-delivery/
|
||||
[redecentralize]: http://redecentralize.org/about/
|
||||
[brewster]: https://blog.archive.org/2015/02/11/locking-the-web-open-a-call-for-a-distributed-web/
|
||||
|
||||
## Who is using WebTorrent today?
|
||||
|
||||
WebTorrent is still pretty new, but it's already being used in cool ways:
|
||||
|
||||
- **[WebTorrent Desktop][webtorrent-desktop]** - Streaming torrent app. For Mac, Windows, and Linux. ([source code][webtorrent-desktop-source])
|
||||
- **[Wormhole][wormhole]** – Simple, private file sharing (built by the WebTorrent team)
|
||||
- **[Instant.io][instant.io]** – Streaming file transfer over WebTorrent ([source code][instant.io-source])
|
||||
- **[GitTorrent][gittorrent]** - Decentralized GitHub using BitTorrent and Bitcoin ([source code][gittorrent-source])
|
||||
- **[File.pizza][filepizza]** - Free peer-to-peer file transfers in your browser ([source code][filepizza-source])
|
||||
- **[Webtorrentapp][webtorrentapp]** – Platform for launching web apps from torrents
|
||||
- **[Fastcast][fastcast]** – Gallery site with some videos ([source code][fastcast-source])
|
||||
- **[Tokenly Pockets][pockets]** - Digital token issuance with WebTorrent-based metadata ([source code][pockets-source])
|
||||
- **[βTorrent][btorrent]** - Fully-featured browser WebTorrent client ([source code][btorrent-source])
|
||||
- **[PeerWeb][peerweb]** - Fetch and render a static website from a torrent
|
||||
- **[YouShark][youshark]** - Web music player for WebTorrent ([source code][youshark-source])
|
||||
- **[Twister][twister]** - Decentralized microblogging service, using WebTorrent for media attachments ([source code][twister-source])
|
||||
- **[PeerTube][peertube]** - Prototype of a decentralized video streaming platform in the web browser ([source code][peertube-source])
|
||||
- **[webtorrent-cljs][webtorrent-cljs]** - Clojurescript wrapper for WebTorrent
|
||||
- **[Squidlink][squidlink]** - Transfer files from A to B without the Cloud ([source code][squidlink-source])
|
||||
- **[Web2web][web2web]** - Server-less & domain-less websites updatable via torrents and bitcoin blockchain ([source code][web2web-source])
|
||||
- **[Magnet Player][magnet-player]** - Stream video torrents directly from your browser ([source code][magnet-player-source])
|
||||
- **[PeerFast][peerfast]** - First P2P Internet Speed Test ([source code][peerfast-source])
|
||||
- **[TorrentMedia][torrentmedia]** - Fully-featured desktop WebTorrent client
|
||||
- **[Gaia 3D Star Map][gaia]** - 2 million stars, rendered in 3D with WebGL, WebVR, and WebTorrent
|
||||
- **[Watchtor][watchtor]** - A minimalistic approach for online torrent watching ([source code][watchtor-source])
|
||||
- **[FileMap][filemap]** - Share files by pinning them to geographic locations
|
||||
- **[WebTorrent Google Cast (WTGC)][wtgc]** - Play WebTorrent media on Google Cast devices ([source code][wtgc-source])
|
||||
- **[CodeDump][codedump]** - A WebTorrent based code pastebin ([source code][codedump-source])
|
||||
- **[Lunik-Torrent][lunik-torrent]** - WebTorrent downloader and file manager. ([source code][lunik-torrent-source])
|
||||
- **[BitChute][bitchute]** - A decentralized video streaming social network
|
||||
- **[Planktos][planktos]** - Enables websites to serve their static content over BitTorrent ([source code][planktos-source])
|
||||
- **[P2P-CDN][p2pcdn]** - WebTorrent CDN with graceful degradation
|
||||
- **[PearPlayer][PearPlayer]** - A WebTorrent based multi-source and multi-protocol P2P streaming media player
|
||||
- **[Tcloud][tcloud]** - File sharing and torrent downloading
|
||||
- **[Webtorrent-webui][webtorrent-webui]** - A WebTorrent client with a simple web interface for easy remote usage
|
||||
- **[CineTimes][cinetimes]** - A streaming website of public domain movies
|
||||
- **[Bitlove.org][bitlove]** - Your favorite podcasts via BitTorrent
|
||||
- **[Live-torrent][live-torrent]** - Simple implementation of a webtorrent powered live streaming solution ([source code][live-torrent-source])
|
||||
- **[CDNBye][CDNBye]** - CDNBye implements WebRTC datachannel to scale live/vod video streaming by peer-to-peer network using bittorrent-like protocol.
|
||||
- **[Files.fm][Files.fm]** - a fast file sharing and freemium cloud storage service that uses P2P technology to accelerate unlimited downloads and file distribution.
|
||||
- **[imgest][imgest]** - Serverless shareable image gallery built with JavaScript and WebTorrent.
|
||||
- **[Bugout][Bugout]** - build and run back-end web services in a browser tab.
|
||||
- **[P2P Media Loader][p2p-media-loader]** - engine for Hls.js and Shaka Player that enables P2P sharing of live and VOD streams over HLS or DASH protocols.
|
||||
- **[Hubzilla][hubzilla]** - WebTorrent player integration into posts ([source code][hubzilla-source])
|
||||
- **[Come Over][comeover]** - Video stream sharing to watch movies together ([source code][comeover-source])
|
||||
- **[PeerWebSite][peerwebsite]** - Peer to Peer Web Site hosting at your fingertips! Send full featured HTML (incl. CSS, JS) sites from your browser and attach files eg. videos, images, etc.
|
||||
- **[CipherTorrent][cipher-torrent]** - Online and offline browser torrent client ([source code][cipher-torrent-source])
|
||||
- **[Slingcode][Slingcode]** - make, run, and share web apps P2P in the browser.
|
||||
- **[Torrent🧲Parts][TorrentParts]** - A website to inspect and edit what's in your Torrent file or Magnet link
|
||||
- **[Live On Torrent][liveontorrent]** - A free plataform to live streaming on browser.
|
||||
- **[WebTorrentPlayer][webtorrentplayer]** - High performance, no compromise video player for WebTorrent ([source code][webtorrentplayer-source])
|
||||
- **[Storm][storm]** - A beautiful torrent client for desktop.
|
||||
- **[atorable-loader][atorable-loader-source]** - Resolves Webpack import/require() of a file into a Webtorrent magnet uri.
|
||||
- **[atorable-react][atorable-react]** - React component that processes a Webtorrent magnet uri for viewing or other custom uses. ([source code][atorable-react-source])
|
||||
- **[Iris][iris-messenger]** - Decentralized social networking application. ([source code][iris-messenger-source])
|
||||
- **[Miru][miru-source]** - Stream anime torrents, real-time with no waiting for downloads. ([source code][miru-source])
|
||||
- **[Haven Torrent Client][haven-torrent-client]** - Simple and Fast Torrent Client for the web. ([source code][haven-torrent-client-source])
|
||||
- **[CrawFish][CrawFish]** - Desktop/Web/Server torrent client, with streaming support and integrated search (Works in docker, windows and has a WebUI that can be accessed by remote). ([source code][CrawFish-source])
|
||||
- **[Niwder][Niwder]** - Web based platform to transfer torrents to Mega.nz and Google Drive on the cloud. ([source code][Niwder-source])
|
||||
- **[Chitchatter][Chitchatter]** - A peer-to-peer chat app that is serverless, decentralized, and ephemeral. Uses WebTorrent to initiate peer connections. ([source code][Chitchatter-source])
|
||||
- **[P2PFileShare][P2PFileShare]** - A peer-to-peer file-sharing app that allows users to send and receive files directly from your browser.
|
||||
- **_Your app here – [Send a pull request][pr] with your URL!_**
|
||||
<!-- - **[PeerCloud][peercloud]** - Serverless websites via WebTorrent ([source code][peercloud-source]) -->
|
||||
<!-- - **[Niagara][niagara]** - Video player webtorrent with subtitles (zipped .srt(s)) -->
|
||||
<!-- - **[Vique][vique]** - Video player queue to share videos -->
|
||||
|
||||
#### WebTorrent Product Alternatives
|
||||
|
||||
There's also a list of WebTorrent-powered alternatives to centralized services here: [WebTorrent Product Clones][webtorrent-clones]
|
||||
|
||||
[webtorrent-clones]: https://github.com/DiegoRBaquero/awesome-webtorrent-clones
|
||||
[webtorrent-desktop]: https://webtorrent.io/desktop
|
||||
[webtorrent-desktop-source]: https://github.com/webtorrent/webtorrent-desktop
|
||||
[wormhole]: https://wormhole.app
|
||||
[instant.io-source]: https://github.com/webtorrent/instant.io
|
||||
[gittorrent]: http://blog.printf.net/articles/2015/05/29/announcing-gittorrent-a-decentralized-github/
|
||||
[gittorrent-source]: https://github.com/cjb/GitTorrent
|
||||
[filepizza]: http://file.pizza/
|
||||
[filepizza-source]: https://github.com/kern/filepizza
|
||||
[peercloud]: https://peercloud.io/
|
||||
[peercloud-source]: https://github.com/jhiesey/peercloud
|
||||
[webtorrentapp]: https://github.com/alexeisavca/webtorrentapp
|
||||
[fastcast]: http://fastcast.nz
|
||||
[fastcast-source]: https://github.com/fastcast/fastcast
|
||||
[pockets]: https://tokenly.com/
|
||||
[pockets-source]: https://github.com/loon3/Tokenly-Pockets
|
||||
[btorrent]: https://btorrent.xyz
|
||||
[btorrent-source]: https://github.com/DiegoRBaquero/bTorrent
|
||||
[peerweb]: https://github.com/retrohacker/peerweb.js
|
||||
[niagara]: https://andreapaiola.name/niagara/
|
||||
[vique]: https://andreapaiola.name/vique/
|
||||
[youshark]: http://youshark.neocities.org/
|
||||
[youshark-source]: https://github.com/enorrmann/youshark
|
||||
[twister]: http://twister.net.co/?p=589
|
||||
[twister-source]: https://github.com/miguelfreitas/twister-html
|
||||
[peertube]: http://peertube.cpy.re
|
||||
[peertube-source]: https://github.com/Chocobozzz/PeerTube
|
||||
[webtorrent-cljs]: https://github.com/cvillecsteele/webtorrent-cljs
|
||||
[squidlink]: http://squidl.ink
|
||||
[squidlink-source]: https://github.com/darkenvy/Squidl.ink
|
||||
[web2web]: https://elendirx.github.io/web2web
|
||||
[web2web-source]: https://github.com/elendirx/web2web
|
||||
[magnet-player]: https://ferrolho.github.io/magnet-player/
|
||||
[magnet-player-source]: https://github.com/ferrolho/magnet-player/
|
||||
[peerfast]: https://diegorbaquero.github.io/PeerFast/#
|
||||
[peerfast-source]: https://github.com/DiegoRBaquero/PeerFast
|
||||
[torrentmedia]: https://github.com/FaCuZ/torrentmedia
|
||||
[gaia]: http://charliehoey.com/threejs-demos/gaia_dr1.html
|
||||
[watchtor]: https://open-watchtor.hashbase.io
|
||||
[watchtor-source]: https://github.com/codealchemist/watchtor
|
||||
[filemap]: https://filemap.xyz
|
||||
[wtgc]: https://wtgc.firebaseapp.com
|
||||
[wtgc-source]: https://github.com/FluorescentHallucinogen/webtorrent-googlecast
|
||||
[codedump]: http://ronsoros.github.io
|
||||
[codedump-source]: https://github.com/ronsoros/ronsoros.github.io/blob/master/index.html
|
||||
[lunik-torrent]: https://tcloud-lunik.herokuapp.com
|
||||
[lunik-torrent-source]: https://github.com/Lunik/Lunik-Torrent
|
||||
[bitchute]: https://www.bitchute.com
|
||||
[planktos]: https://xuset.github.io/planktos/
|
||||
[planktos-source]: https://github.com/xuset/planktos
|
||||
[p2pcdn]: https://github.com/andreapaiola/P2P-CDN
|
||||
[PearPlayer]: https://github.com/PearInc/PearPlayer.js
|
||||
[tcloud]: https://github.com/Lunik/tcloud
|
||||
[webtorrent-webui]: https://github.com/pldubouilh/webtorrent-webui
|
||||
[cinetimes]: http://cinetimes.org/
|
||||
[bitlove]: https://bitlove.org/
|
||||
[live-torrent]: https://live.computer
|
||||
[live-torrent-source]: https://github.com/pldubouilh/live-torrent
|
||||
[CDNBye]: https://github.com/cdnbye/hlsjs-p2p-engine
|
||||
[Files.fm]: https://files.fm
|
||||
[imgest]: https://imgest.hashbase.io
|
||||
[Bugout]: https://github.com/chr15m/bugout
|
||||
[Slingcode]: https://github.com/chr15m/slingcode
|
||||
[p2p-media-loader]: https://github.com/novage/p2p-media-loader
|
||||
[hubzilla]: https://hubzilla.org
|
||||
[hubzilla-source]: https://github.com/demitas-ace/wtplayer/tree/master/wtplayer
|
||||
[comeover]: https://luccadoret.github.io/comeover/home
|
||||
[comeover-source]: https://github.com/LucCADORET/comeover
|
||||
[peerwebsite]: https://peerweb.site
|
||||
[cipher-torrent]: https://torrent.cipherdogs.net
|
||||
[cipher-torrent-source]: https://github.com/CipherDogs/cipher-torrent
|
||||
[TorrentParts]: https://torrent.parts
|
||||
[liveontorrent]: https://www.weboscoder.com/liveontorrent/
|
||||
[webtorrentplayer]: https://thaunknown.github.io/webtorrent-player/
|
||||
[webtorrentplayer-source]: https://github.com/ThaUnknown/webtorrent-player
|
||||
[storm]: https://github.com/nuzzesick/storm-desktop
|
||||
[atorable-loader-source]: https://github.com/Atorable/atorable-loader
|
||||
[atorable-react]: https://atorable.github.io/atorable-react/
|
||||
[atorable-react-source]: https://github.com/Atorable/atorable-react
|
||||
[iris-messenger]: https://iris.to
|
||||
[iris-messenger-source]: https://github.com/irislib/iris-messenger
|
||||
[CrawFish]: https://github.com/drakonkat/crawfish/blob/main/README.md
|
||||
[CrawFish-source]: https://github.com/drakonkat/crawfish
|
||||
[miru-source]: https://github.com/ThaUnknown/miru
|
||||
[haven-torrent-client]: https://haven.pages.dev/torrent-client/public/
|
||||
[haven-torrent-client-source]: https://github.com/ThaUnknown/pwa-haven/tree/main/torrent-client
|
||||
[Niwder]: https://niwder.niweera.gq
|
||||
[Niwder-source]: https://github.com/Niweera/niwder
|
||||
[Chitchatter]: https://chitchatter.im/
|
||||
[Chitchatter-source]: https://github.com/jeremyckahn/chitchatter
|
||||
[P2PFileShare]: https://p2pfileshare.com
|
||||
|
||||
## How does WebTorrent work?
|
||||
|
||||
The WebTorrent protocol works just like [BitTorrent protocol][bittorrent-protocol],
|
||||
except it uses [WebRTC][webrtc] instead of [TCP][tcp]/[uTP][utp] as the transport
|
||||
protocol.
|
||||
|
||||
In order to support [WebRTC's connection model][webrtc-signaling], we made a few
|
||||
changes to the tracker protocol. Therefore, a browser-based WebTorrent client or
|
||||
**"web peer"** can only connect to other clients that support WebTorrent/WebRTC.
|
||||
|
||||
The protocol changes we made will be published as a
|
||||
[BEP](http://www.bittorrent.org/beps/bep_0001.html). Until a spec is written, you
|
||||
can view the source code of the [`bittorrent-tracker`][bittorrent-tracker] package.
|
||||
|
||||
Once peers are connected, the wire protocol used to communicate is exactly the same
|
||||
as in normal BitTorrent. This should make it easy for existing popular torrent
|
||||
clients like Transmission, and uTorrent to add support for WebTorrent. **Vuze**
|
||||
[already has support][vuze-support] for WebTorrent!
|
||||
|
||||

|
||||
|
||||
[bittorrent-protocol]: https://wiki.theory.org/BitTorrentSpecification
|
||||
[webrtc-signaling]: http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/#what-is-signaling
|
||||
[tcp]: https://en.wikipedia.org/wiki/Transmission_Control_Protocol
|
||||
[utp]: https://en.wikipedia.org/wiki/Micro_Transport_Protocol
|
||||
[webrtc]: https://en.wikipedia.org/wiki/WebRTC
|
||||
[bittorrent-tracker]: https://npmjs.com/package/bittorrent-tracker
|
||||
[vuze-support]: https://wiki.vuze.com/w/WebTorrent
|
||||
|
||||
## How do I get started?
|
||||
|
||||
To start using WebTorrent, simply include the
|
||||
[`webtorrent.min.js`](https://cdn.jsdelivr.net/npm/webtorrent@latest/webtorrent.min.js)
|
||||
script on your page. If you use [browserify](http://browserify.org/) or [webpack](https://webpack.js.org/), you can
|
||||
`npm install webtorrent` and `import WebTorrent from 'webtorrent'`.
|
||||
|
||||
It's easy to download a torrent and add it to the page.
|
||||
|
||||
```js
|
||||
const client = new WebTorrent();
|
||||
|
||||
const torrentId =
|
||||
"magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent";
|
||||
|
||||
// see tutorials.md for a full example of streaming media using service workers
|
||||
navigator.serviceWorker.register("sw.min.js");
|
||||
const controller = await navigator.serviceWorker.ready;
|
||||
client.createServer({ controller });
|
||||
|
||||
client.add(torrentId, (torrent) => {
|
||||
// Torrents can contain many files. Let's use the .mp4 file
|
||||
const file = torrent.files.find((file) => {
|
||||
return file.name.endsWith(".mp4");
|
||||
});
|
||||
file.streamTo(document.querySelector("video")); // append the file to the DOM
|
||||
});
|
||||
```
|
||||
|
||||
This supports video, audio, images, PDFs, Markdown, [and more][render-media], right
|
||||
out of the box. There are additional ways to access file content directly, including
|
||||
as a node-style stream, Buffer, or Blob URL.
|
||||
|
||||
Video and audio content can be streamed, i.e. playback will start before the full
|
||||
file is downloaded. Seeking works too – WebTorrent dynamically fetches
|
||||
the needed torrent pieces from the network on-demand.
|
||||
|
||||
## What is WebRTC?
|
||||
|
||||
WebRTC (Web Real-Time Communication) is an API defined by the World Wide Web
|
||||
Consortium (W3C) to support browser-to-browser applications like voice calling,
|
||||
video chat, and P2P file sharing without the need for browser plugins.
|
||||
|
||||
WebRTC's `RTCDataChannel` API allows the transfer of data directly from one browser
|
||||
to another. This is distinct from `WebSocket` and `XMLHttpRequest` because these are
|
||||
designed for communication to/from a server, i.e. a client-server model. Data
|
||||
Channels allow for **direct browser-to-browser connections**.
|
||||
|
||||
This is revolutionary. Never before could websites connect their users directly to
|
||||
each other with super low-latency, encrypted, peer-to-peer connections. This will
|
||||
enable next-generation applications in healthcare, education, science, and more.
|
||||
WebTorrent is just one example.
|
||||
|
||||
WebRTC [works everywhere][webrtc-everywhere], and browser support is excellent.
|
||||
**Chrome**, **Firefox**, and **Opera** for Desktop and Android, as well as
|
||||
**Microsoft Edge** and **Safari** have support.
|
||||
|
||||
You can learn more about WebRTC data channels at [HTML5Rocks][datachannel-intro].
|
||||
|
||||
[webrtc-everywhere]: https://speakerdeck.com/feross/webrtc-everywhere-beyond-the-browser-at-data-terra-nemo-2015
|
||||
[datachannel-intro]: http://www.html5rocks.com/en/tutorials/webrtc/datachannels/
|
||||
|
||||
## Can WebTorrent clients connect to normal BitTorrent clients?
|
||||
|
||||
In the browser, WebTorrent can only download torrents that are seeded by a
|
||||
WebRTC-capable torrent client.
|
||||
|
||||
Right now, we know of these WebRTC-capable torrent clients:
|
||||
|
||||
- **[WebTorrent Desktop][webtorrent-desktop]** - Open source streaming torrent client. For Mac, Windows, and Linux.
|
||||
- **[Vuze][vuze-support]** - Powerful, full-featured torrent client
|
||||
- **[Playback][playback]** - Open source JavaScript video player **(super cool!)**
|
||||
- **[`webtorrent-hybrid`][webtorrent-hybrid]** - Node.js package (command line and API)
|
||||
- **[Instant.io][instant.io]** - Simple WebTorrent client in a website
|
||||
- **[βTorrent][btorrent]** - Fully-featured browser WebTorrent client ([source code][btorrent-source])
|
||||
- **[TorrentMedia][torrentmedia]** - Desktop WebTorrent client
|
||||
- _More coming soon – [Send a PR][pr] to add your client to the list!_
|
||||
|
||||
### A bit more about `webtorrent-hybrid`
|
||||
|
||||
In node.js, `webtorrent-hybrid` can download torrents from WebRTC peers or TCP peers
|
||||
(i.e. normal peers). You can use WebTorrent as a command line program, or
|
||||
programmatically as a node.js package.
|
||||
|
||||
To install `webtorrent-hybrid` run the following command in your terminal (add the
|
||||
`-g` flag to install the command line program, omit it to install locally):
|
||||
|
||||
```
|
||||
npm install webtorrent-hybrid -g
|
||||
```
|
||||
|
||||
Note: If you just need to use WebTorrent in the browser (where WebRTC is available
|
||||
natively) then use [`webtorrent`][webtorrent] instead, which is faster to install
|
||||
because it won't need to install a WebRTC implementation.
|
||||
|
||||
## Can WebTorrent clients on different websites connect to each other?
|
||||
|
||||
Yes! **WebTorrent works across the entire web.** WebTorrent clients running on one
|
||||
domain can connect to clients on any other domain. No silos!
|
||||
|
||||
The same-origin policy does not apply to WebRTC connections since they are not
|
||||
client-to-server. Browser-to-browser connections require the cooperation of both
|
||||
websites (i.e. the WebTorrent script must be present on both sites).
|
||||
|
||||
## Who builds WebTorrent?
|
||||
|
||||
WebTorrent is built by [Feross Aboukhadijeh][feross] and hundreds of open source
|
||||
contributors. The WebTorrent project is managed by
|
||||
[WebTorrent, LLC][webtorrent-io], as a non-profit project.
|
||||
|
||||
Feross's other projects include [JavaScript Standard Style][standard],
|
||||
[PeerCDN][peercdn] (sold to Yahoo), [Study Notes][studynotes], and
|
||||
[YouTube Instant][ytinstant].
|
||||
|
||||
In the past, Feross attended [Stanford University][stanford], did research in the
|
||||
[Stanford Human-Computer Interaction][hci] and [Computer Security][seclab] labs,
|
||||
and worked at [Quora][quora], [Facebook][facebook], and [Intel][intel].
|
||||
|
||||
[standard]: http://standardjs.com/
|
||||
[studynotes]: https://www.apstudynotes.org/
|
||||
[ytinstant]: http://ytinstant.com/
|
||||
[stanford]: http://www.stanford.edu/
|
||||
[hci]: http://hci.stanford.edu/
|
||||
[seclab]: http://seclab.stanford.edu/
|
||||
[quora]: https://www.quora.com/
|
||||
[facebook]: https://www.facebook.com/
|
||||
[intel]: http://intel.com/
|
||||
|
||||
## What is WebTorrent, LLC?
|
||||
|
||||
"WebTorrent, LLC" is the legal entity that owns WebTorrent. WebTorrent is, and
|
||||
always will be, **non-profit, open source, and free software**.
|
||||
|
||||
There are no plans to make a profit from WebTorrent.
|
||||
|
||||
## How is WebTorrent different from PeerCDN?
|
||||
|
||||
[PeerCDN][peercdn] was a next-generation CDN powered by WebRTC for efficient
|
||||
peer-to-peer delivery of website content. PeerCDN was founded by
|
||||
[Feross Aboukhadijeh][feross], [Abi Raja][abi], and [John Hiesey][jhiesey] in
|
||||
March 2013 and was sold to [Yahoo][yahoo] in December 2013.
|
||||
|
||||
WebTorrent is an independent project started by [Feross Aboukhadijeh][feross] in
|
||||
October 2013. Unlike PeerCDN, **WebTorrent is free software**, licensed under the
|
||||
[MIT License][license]. You're free to use it however you like!
|
||||
|
||||
> "Free software" is a matter of liberty, not price. To understand the concept, you
|
||||
> should think of "free" as in "free speech," not as in "free beer."
|
||||
>
|
||||
> <cite>— Richard Stallman, software freedom activist</cite>
|
||||
|
||||
On a technical level, PeerCDN and WebTorrent were built with different goals in
|
||||
mind. PeerCDN was optimized for low-latency downloads and fast peer discovery. This
|
||||
meant the client and site owner trusted centralized servers to map file URLs to
|
||||
content hashes.
|
||||
|
||||
WebTorrent, on the other hand, doesn't require clients to trust a centralized
|
||||
server. Given a `.torrent` file or magnet link, the WebTorrent client downloads the
|
||||
file without trusting servers or peers at any point.
|
||||
|
||||
[feross]: http://feross.org/
|
||||
[abi]: http://abiraja.com/
|
||||
[jhiesey]: https://github.com/jhiesey
|
||||
[yahoo]: https://www.yahoo.com/
|
||||
|
||||
## How can I contribute?
|
||||
|
||||
WebTorrent is an **OPEN Open Source Project**. Individuals who make significant and
|
||||
valuable contributions are given commit access to the project to contribute as they
|
||||
see fit. (See the full [contributor guidelines][contributing].)
|
||||
|
||||
There are many ways to help out!
|
||||
|
||||
- Report bugs by [creating a GitHub issue][issues].
|
||||
- Write code to [fix an open issue][open-issues].
|
||||
|
||||
If you're looking for help getting started, come join us in [Gitter][gitter] or on
|
||||
IRC at `#webtorrent` (freenode) and how you can get started.
|
||||
|
||||
[open-issues]: https://github.com/webtorrent/webtorrent/issues?state=open
|
||||
[contributing]: https://github.com/webtorrent/.github/blob/master/CONTRIBUTING.md
|
||||
|
||||
## Where can I learn more?
|
||||
|
||||
There are many talks online about WebTorrent. Here are a few:
|
||||
|
||||
### Intro to BitTorrent and WebTorrent (JSConf)
|
||||
|
||||
<iframe width="853" height="480" src="https://www.youtube.com/embed/kxHRATfvnlw?rel=0" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
### WebRTC Everywhere: Beyond the Browser (slides only)
|
||||
|
||||
<script async class="speakerdeck-embed" data-id="cb08869f2ac2445c99e8b73a4ac65d2b" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>
|
||||
|
||||
## WebTorrent supports sequential streaming. How does this affect the network?
|
||||
|
||||
BitTorrent clients select which file pieces to download using an algorithm called
|
||||
"rarest-first". With every peer in the system trying to download the rarest pieces
|
||||
first, on average most pieces will have approximately the same availability in the
|
||||
network.
|
||||
|
||||
In practice, the rarest-first algorithm is most important on poorly-seeded
|
||||
torrents, or in the first few hours of a torrent being published (when the ratio of
|
||||
seeders to leechers is bad).
|
||||
|
||||
Most torrent clients support features that cause it to deviate from a pure rarest-
|
||||
first selection algorithm. For example, the ability to select/deselect or
|
||||
prioritize/deprioritize certain files in the torrent.
|
||||
|
||||
WebTorrent supports streaming a torrent file "in order", which is useful for
|
||||
playing back media files. We’re working on improving the algorithm to switch back
|
||||
to a rarest-first strategy when there is not a high-priority need for specific
|
||||
pieces. In other words, when sufficient media is buffered, we can use the normal
|
||||
"rarest-first" piece selection algorithm.
|
||||
|
||||
But the fact is that with the speed of today’s internet connections, the user is
|
||||
going to finish fully downloading the torrent in a fraction of the time it takes to
|
||||
consume it, so they will still spend more time seeding than downloading.
|
||||
|
||||
Also note: BitTorrent Inc.'s official torrent client, uTorrent, offers sequential
|
||||
downloading, as well as selective file downloading, and the BitTorrent network
|
||||
remains very healthy.
|
||||
|
||||
## Why wasn't WebTorrent designed as an entirely-new, modern protocol?
|
||||
|
||||
BitTorrent is the most successful, most widely-deployed P2P protocol in existence.
|
||||
It works really well. Our goal with WebTorrent was to bring BitTorrent to the web
|
||||
in a way that interoperates with the existing torrent network.
|
||||
|
||||
Re-inventing the protocol would have made WebTorrent fundamentally incompatible
|
||||
with existing clients and prevented adoption. The way we've done it is better. The
|
||||
wire protocol is exactly the same, but there's now a new way to connect to peers:
|
||||
WebRTC, in addition to the existing TCP and uTP.
|
||||
|
||||
Also, re-inventing the protocol is a huge rabbit hole. There was already a lot of
|
||||
risk when we started the project -- will WebRTC get adopted by all the browser
|
||||
vendors? Will the Data Channel implementation stabilize and be performant? Is
|
||||
JavaScript fast enough to re-package MP4 videos on-the-fly for streaming playback
|
||||
with the MediaSource API? Our thinking was: Why add inventing a new wire protocol
|
||||
and several algorithms to the table?
|
||||
|
||||
It's true that the BitTorrent protocol is dated in some ways. For example, it uses
|
||||
it's own strange data encoding called "bencoding". If it were invented today, it
|
||||
would probably just use JSON or MessagePack. But, this doesn't matter -- BitTorrent
|
||||
works really well, and we care more about building robust and useful software than
|
||||
conceptual purity or the latest software fashions.
|
||||
|
||||
## Is it possible to do live streaming with WebTorrent?
|
||||
|
||||
WebTorrent cannot do live streaming out-of-the-box, however you can build a live
|
||||
streaming solution on top of WebTorrent.
|
||||
|
||||
Torrents are immutable. That means that once a torrent file is created, it cannot
|
||||
be changed without changing the info hash. So, how could one get around this
|
||||
limitation?
|
||||
|
||||
A naive approach would be this: The content producer could take every 10 seconds of
|
||||
live content and create a torrent for it. Viewers would follow this "feed" of
|
||||
torrent files (or info hashes) and download the content sequentially. Streamers
|
||||
would be around 10-20 seconds behind the live stream.
|
||||
|
||||
This approach can definitely be improved, though! Why not give that a shot yourself
|
||||
and share the code?
|
||||
|
||||
## Does WebTorrent leak your IP address when using a VPN? I heard that WebRTC leaks your IP address.
|
||||
|
||||
No.
|
||||
|
||||
WebRTC data channels do not allow a website to discover your public IP address when
|
||||
there is a VPN in use. The WebRTC discovery process will just find your VPN's IP
|
||||
address and the local network IP address.
|
||||
|
||||
Local IP addresses (e.g. 10.x.x.x or 192.168.x.x) can potentially be used to
|
||||
"fingerprint" your browser and identify across different sites that you visit,
|
||||
like a third-party tracking cookie. However, this is a separate issue than exposing
|
||||
your real public IP address, and it's worth noting that the browser already
|
||||
provides hundreds of vectors for fingerprinting you
|
||||
(e.g. your installed fonts, screen resolution, browser window size, OS version,
|
||||
language, etc.).
|
||||
|
||||
If you have a VPN enabled, then WebRTC data channels will not connect to peers
|
||||
using your true public IP address, nor will it be revealed to the JavaScript running
|
||||
on the webpage.
|
||||
|
||||
At one point in time, WebRTC did have an issue where it would allow a website
|
||||
to discover your true public IP address, but this was fixed a long time ago. This
|
||||
unfortunate misinformation keeps bouncing around the internet.
|
||||
|
||||
There's now a spec that defines exactly which IP addresses are exposed with WebRTC.
|
||||
If you're interested in further reading, you can read the
|
||||
[IP handling spec](https://tools.ietf.org/html/draft-ietf-rtcweb-ip-handling-01)
|
||||
for yourself.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
## Why does browser downloading not work? I see no peers!
|
||||
|
||||
It does work! But you can't just use any random magnet uri or `.torrent` file. The
|
||||
torrent must be seeded by a WebRTC-capable client, i.e.
|
||||
[WebTorrent Desktop][webtorrent-desktop], [Vuze][vuze-support],
|
||||
[webtorrent-hybrid][webtorrent-hybrid], [Playback][playback],
|
||||
[instant.io][instant.io], or [βTorrent][btorrent].
|
||||
|
||||
In the browser, WebTorrent can only download torrents that are explicitly seeded to
|
||||
web peers via a WebRTC-capable client. Desktop torrent clients need to support
|
||||
WebRTC to connect to web browsers.
|
||||
|
||||
## Why does video/audio streaming not work?
|
||||
|
||||
Streaming support depends on support for `MediaSource` API in the browser. All
|
||||
modern browsers have `MediaSource` support. In Firefox, support was added in
|
||||
Firefox 42 (i.e. Firefox Nightly).
|
||||
|
||||
[Many file types][render-media] are supported (again, depending on browser support),
|
||||
but only `.mp4`, `.m4v`, and `.m4a` have full support, including seeking.
|
||||
|
||||
To support video/audio streaming of arbitrary files, WebTorrent uses the
|
||||
[`videostream`][videostream] package, which in turn uses [`mp4box.js`][mp4box.js].
|
||||
If you think there may be a bug in one of these packages, please file an issue on
|
||||
the respective repository.
|
||||
|
||||
[videostream]: https://npmjs.com/package/videostream
|
||||
[mp4box.js]: https://github.com/gpac/mp4box.js
|
||||
|
||||
## Got more questions?
|
||||
|
||||
Open an issue on the WebTorrent [issue tracker][issues], or join us in
|
||||
[Gitter][gitter] or on IRC at `#webtorrent` (freenode).
|
||||
|
||||
[webtorrent-io]: https://webtorrent.io
|
||||
[render-media]: https://github.com/feross/render-media/blob/master/index.js
|
||||
[gitter]: https://gitter.im/webtorrent/webtorrent
|
||||
[instant.io]: https://instant.io
|
||||
[issues]: https://github.com/webtorrent/webtorrent/issues
|
||||
[license]: https://github.com/webtorrent/webtorrent/blob/master/LICENSE
|
||||
[peercdn]: http://www.peercdn.com/
|
||||
[playback]: https://mafintosh.github.io/playback/
|
||||
[pr]: https://github.com/webtorrent/webtorrent
|
||||
[webtorrent-hybrid]: https://npmjs.com/package/webtorrent-hybrid
|
||||
[webtorrent]: https://npmjs.com/package/webtorrent
|
@@ -1,457 +0,0 @@
|
||||
# Get Started with WebTorrent
|
||||
|
||||
**WebTorrent** is the first torrent client that works in the **browser**. It's easy
|
||||
to get started!
|
||||
|
||||
## Install
|
||||
|
||||
To start using WebTorrent, simply include the
|
||||
[`webtorrent`](https://esm.sh/webtorrent)
|
||||
script on your page.
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import WebTorrent from "https://esm.sh/webtorrent";
|
||||
</script>
|
||||
```
|
||||
|
||||
### Browserify and Webpack
|
||||
|
||||
WebTorrent also works great with [browserify](http://browserify.org/), [webpack](https://webpack.js.org/) and other bundlers, which let
|
||||
you use [node.js](http://nodejs.org/) style `require()` to organize your browser
|
||||
code, and load packages installed by [npm](https://npmjs.org/).
|
||||
|
||||
For an example webpack config see [the webpack bundle config used by webtorrent](/scripts/browser.webpack.js).
|
||||
|
||||
```
|
||||
npm install webtorrent
|
||||
```
|
||||
|
||||
Then use `WebTorrent` like this:
|
||||
|
||||
```js
|
||||
import WebTorrent from "webtorrent";
|
||||
```
|
||||
|
||||
## Quick Examples
|
||||
|
||||
### Downloading a torrent (in the browser)
|
||||
|
||||
```js
|
||||
import WebTorrent from "webtorrent";
|
||||
|
||||
const client = new WebTorrent();
|
||||
|
||||
// Sintel, a free, Creative Commons movie
|
||||
const torrentId =
|
||||
"magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent";
|
||||
|
||||
// see tutorials.md for a full example of streaming media using service workers
|
||||
navigator.serviceWorker.register("sw.min.js");
|
||||
const controller = await navigator.serviceWorker.ready;
|
||||
client.createServer({ controller });
|
||||
|
||||
client.add(torrentId, (torrent) => {
|
||||
// Torrents can contain many files. Let's use the .mp4 file
|
||||
const file = torrent.files.find((file) => {
|
||||
return file.name.endsWith(".mp4");
|
||||
});
|
||||
|
||||
// Display the file by adding it to the DOM.
|
||||
// Supports video, audio, image files, and more!
|
||||
file.streamTo(document.querySelector("video"));
|
||||
});
|
||||
```
|
||||
|
||||
This supports video, audio, images, PDFs, HTML, right out of the box. There are additional ways to access file content directly, including as a node-style stream, ArrayBuffer, or Blob.
|
||||
|
||||
Video and audio content can be streamed, i.e. playback will start before the full file is downloaded. Seeking works too – WebTorrent dynamically fetches
|
||||
the needed torrent pieces from the network on-demand.
|
||||
|
||||
**Note:** Downloading a torrent automatically seeds it, making it available for download by other peers.
|
||||
|
||||
### Creating a new torrent and seed it (in the browser)
|
||||
|
||||
```js
|
||||
import dragDrop from "drag-drop";
|
||||
import WebTorrent from "webtorrent";
|
||||
|
||||
const client = new WebTorrent();
|
||||
|
||||
// When user drops files on the browser, create a new torrent and start seeding it!
|
||||
dragDrop("body", (files) => {
|
||||
client.seed(files, (torrent) => {
|
||||
console.log("Client is seeding " + torrent.magnetURI);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This example uses the [`drag-drop`][drag-drop] package, to make the HTML5 Drag and
|
||||
Drop API easier to work with.
|
||||
|
||||
**Note:** If you do not use browserify, use the standalone file
|
||||
[`dragdrop.min.js`](https://bundle.run/drag-drop).
|
||||
This exports a `DragDrop` function on `window`.
|
||||
|
||||
### Download and save a torrent (in Node.js)
|
||||
|
||||
```js
|
||||
import WebTorrent from "webtorrent";
|
||||
|
||||
const client = new WebTorrent();
|
||||
|
||||
const magnetURI = "magnet: ...";
|
||||
|
||||
client.add(magnetURI, { path: "/path/to/folder" }, (torrent) => {
|
||||
torrent.on("done", () => {
|
||||
console.log("torrent download finished");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Creating a new torrent and seed it (in Node.js)
|
||||
|
||||
**Note:** Seeding a torrent that can be downloaded by browser peers (i.e. with support for WebRTC) requires [webtorrent-hybrid](https://github.com/webtorrent/webtorrent-hybrid).
|
||||
|
||||
```js
|
||||
import WebTorrent from "webtorrent-hybrid";
|
||||
const client = new WebTorrent();
|
||||
|
||||
client.seed("/seed-me.txt", (torrent) => {
|
||||
console.log("Client is seeding " + torrent.magnetURI);
|
||||
});
|
||||
```
|
||||
|
||||
where **seed-me.txt** is a text file which is going to be seeded as a torrent.
|
||||
|
||||
### Complete HTML page example
|
||||
|
||||
Looking for a more complete example? Look no further! This HTML example has a form input
|
||||
where the user can paste a magnet link and start a download over WebTorrent.
|
||||
|
||||
Best of all, it's a single HTML page, under 70 lines!
|
||||
|
||||
If the torrent contains images, videos, audio, or other playable files (with supported
|
||||
codecs), they will be added to the DOM and streamed, even before the full content is
|
||||
downloaded.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>
|
||||
Download files using the WebTorrent protocol (BitTorrent over WebRTC).
|
||||
</h1>
|
||||
|
||||
<form>
|
||||
<label for="torrentId">Download from a magnet link: </label>
|
||||
<input
|
||||
name="torrentId"
|
||||
,
|
||||
placeholder="magnet:"
|
||||
value="magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent"
|
||||
/>
|
||||
<button type="submit">Download</button>
|
||||
</form>
|
||||
|
||||
<h2>Log</h2>
|
||||
<div class="log"></div>
|
||||
|
||||
<script type="module">
|
||||
// Include the latest version of WebTorrent
|
||||
import WebTorrent from "https://esm.sh/webtorrent";
|
||||
|
||||
const client = new WebTorrent();
|
||||
|
||||
client.on("error", (err) => {
|
||||
console.error("ERROR: " + err.message);
|
||||
});
|
||||
|
||||
document.querySelector("form").addEventListener("submit", (e) => {
|
||||
e.preventDefault(); // Prevent page refresh
|
||||
|
||||
const torrentId = document.querySelector(
|
||||
"form input[name=torrentId]"
|
||||
).value;
|
||||
log("Adding " + torrentId);
|
||||
client.add(torrentId, onTorrent);
|
||||
});
|
||||
|
||||
async function onTorrent(torrent) {
|
||||
log("Got torrent metadata!");
|
||||
log(
|
||||
"Torrent info hash: " +
|
||||
torrent.infoHash +
|
||||
" " +
|
||||
'<a href="' +
|
||||
torrent.magnetURI +
|
||||
'" target="_blank">[Magnet URI]</a> ' +
|
||||
'<a href="' +
|
||||
URL.createObjectURL(torrent.torrentFileBlob) +
|
||||
'" target="_blank" download="' +
|
||||
torrent.name +
|
||||
'.torrent">[Download .torrent]</a>'
|
||||
);
|
||||
|
||||
// Print out progress every 5 seconds
|
||||
const interval = setInterval(() => {
|
||||
log("Progress: " + (torrent.progress * 100).toFixed(1) + "%");
|
||||
}, 5000);
|
||||
|
||||
torrent.on("done", () => {
|
||||
log("Progress: 100%");
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
// Render all files into to the page
|
||||
for (const file of torrent.files) {
|
||||
try {
|
||||
const blob = await file.blob();
|
||||
document.querySelector(".log").append(file.name);
|
||||
log(
|
||||
'(Blob URLs only work if the file is loaded from a server. "http//localhost" works. "file://" does not.)'
|
||||
);
|
||||
log("File done.");
|
||||
log(
|
||||
'<a href="' +
|
||||
URL.createObjectURL(blob) +
|
||||
'">Download full file: ' +
|
||||
file.name +
|
||||
"</a>"
|
||||
);
|
||||
} catch (err) {
|
||||
if (err) log(err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function log(str) {
|
||||
const p = document.createElement("p");
|
||||
p.innerHTML = str;
|
||||
document.querySelector(".log").appendChild(p);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### HTML example with status showing UI
|
||||
|
||||
This complete HTML example mimics the UI of the
|
||||
[webtorrent.io](https://webtorrent.io) homepage. It downloads the
|
||||
[sintel.torrent](https://webtorrent.io/torrents/sintel.torrent) file, streams it in
|
||||
the browser and outputs some statistics to the user (peers, progress, remaining
|
||||
time, speed...).
|
||||
|
||||
You can try it right now on [CodePen](http://codepen.io/yciabaud/full/XdOeWM/) to
|
||||
see what it looks like and play around with it!
|
||||
|
||||
Feel free to replace `torrentId` with other torrent files, or magnet links, but
|
||||
keep in mind that the browser can only download torrents that are seeded by
|
||||
WebRTC peers (web peers). Use [WebTorrent Desktop](https://webtorrent.io/desktop)
|
||||
or [Instant.io](https://instant.io) to seed torrents to the WebTorrent network.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>WebTorrent video player</title>
|
||||
<style>
|
||||
#output video {
|
||||
width: 100%;
|
||||
}
|
||||
#progressBar {
|
||||
height: 5px;
|
||||
width: 0%;
|
||||
background-color: #35b44f;
|
||||
transition: width 0.4s ease-in-out;
|
||||
}
|
||||
body.is-seed .show-seed {
|
||||
display: inline;
|
||||
}
|
||||
body.is-seed .show-leech {
|
||||
display: none;
|
||||
}
|
||||
.show-seed {
|
||||
display: none;
|
||||
}
|
||||
#status code {
|
||||
font-size: 90%;
|
||||
font-weight: 700;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.is-seed {
|
||||
background-color: #154820;
|
||||
transition: 0.5s 0.5s background-color ease-in-out;
|
||||
}
|
||||
body {
|
||||
background-color: #2a3749;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
#status {
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
padding: 5px;
|
||||
}
|
||||
a:link,
|
||||
a:visited {
|
||||
color: #30a247;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<div id="progressBar"></div>
|
||||
<video id="output" controls></video>
|
||||
</div>
|
||||
<!-- Statistics -->
|
||||
<div id="status">
|
||||
<div>
|
||||
<span class="show-leech">Downloading </span>
|
||||
<span class="show-seed">Seeding </span>
|
||||
<code>
|
||||
<!-- Informative link to the torrent file -->
|
||||
<a
|
||||
id="torrentLink"
|
||||
href="https://webtorrent.io/torrents/sintel.torrent"
|
||||
>sintel.torrent</a
|
||||
>
|
||||
</code>
|
||||
<span class="show-leech"> from </span>
|
||||
<span class="show-seed"> to </span>
|
||||
<code id="numPeers">0 peers</code>.
|
||||
</div>
|
||||
<div>
|
||||
<code id="downloaded"></code>
|
||||
of <code id="total"></code> — <span id="remaining"></span><br />
|
||||
↘<code id="downloadSpeed">0 b/s</code> / ↗<code
|
||||
id="uploadSpeed"
|
||||
>0 b/s</code
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Moment is used to show a human-readable remaining time -->
|
||||
<script src="http://momentjs.com/downloads/moment.min.js"></script>
|
||||
|
||||
<script type="module">
|
||||
// Include the latest version of WebTorrent
|
||||
import WebTorrent from "./webtorrent.min.js";
|
||||
|
||||
const torrentId = "https://webtorrent.io/torrents/sintel.torrent";
|
||||
|
||||
const client = new WebTorrent();
|
||||
|
||||
// HTML elements
|
||||
const $body = document.body;
|
||||
const $progressBar = document.querySelector("#progressBar");
|
||||
const $numPeers = document.querySelector("#numPeers");
|
||||
const $downloaded = document.querySelector("#downloaded");
|
||||
const $total = document.querySelector("#total");
|
||||
const $remaining = document.querySelector("#remaining");
|
||||
const $uploadSpeed = document.querySelector("#uploadSpeed");
|
||||
const $downloadSpeed = document.querySelector("#downloadSpeed");
|
||||
|
||||
navigator.serviceWorker
|
||||
.register("./sw.min.js", { scope: "./" })
|
||||
.then((reg) => {
|
||||
const worker = reg.active || reg.waiting || reg.installing;
|
||||
function checkState(worker) {
|
||||
return (
|
||||
worker.state === "activated" &&
|
||||
client.createServer({ controller: reg }) &&
|
||||
download()
|
||||
);
|
||||
}
|
||||
if (!checkState(worker)) {
|
||||
worker.addEventListener("statechange", ({ target }) =>
|
||||
checkState(target)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function download() {
|
||||
// Download the torrent
|
||||
client.add(torrentId, (torrent) => {
|
||||
// Torrents can contain many files. Let's use the .mp4 file
|
||||
const file = torrent.files.find((file) => {
|
||||
return file.name.endsWith(".mp4");
|
||||
});
|
||||
|
||||
// Stream the file in the browser
|
||||
file.streamTo(document.querySelector("#output"));
|
||||
|
||||
// Trigger statistics refresh
|
||||
torrent.on("done", onDone);
|
||||
setInterval(onProgress, 500);
|
||||
onProgress();
|
||||
|
||||
// Statistics
|
||||
function onProgress() {
|
||||
// Peers
|
||||
$numPeers.innerHTML =
|
||||
torrent.numPeers + (torrent.numPeers === 1 ? " peer" : " peers");
|
||||
|
||||
// Progress
|
||||
const percent = Math.round(torrent.progress * 100 * 100) / 100;
|
||||
$progressBar.style.width = percent + "%";
|
||||
$downloaded.innerHTML = prettyBytes(torrent.downloaded);
|
||||
$total.innerHTML = prettyBytes(torrent.length);
|
||||
|
||||
// Remaining time
|
||||
let remaining;
|
||||
if (torrent.done) {
|
||||
remaining = "Done.";
|
||||
} else {
|
||||
remaining = moment
|
||||
.duration(torrent.timeRemaining / 1000, "seconds")
|
||||
.humanize();
|
||||
remaining =
|
||||
remaining[0].toUpperCase() +
|
||||
remaining.substring(1) +
|
||||
" remaining.";
|
||||
}
|
||||
$remaining.innerHTML = remaining;
|
||||
|
||||
// Speed rates
|
||||
$downloadSpeed.innerHTML =
|
||||
prettyBytes(torrent.downloadSpeed) + "/s";
|
||||
$uploadSpeed.innerHTML = prettyBytes(torrent.uploadSpeed) + "/s";
|
||||
}
|
||||
function onDone() {
|
||||
$body.className += " is-seed";
|
||||
onProgress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Human readable bytes util
|
||||
function prettyBytes(num) {
|
||||
const units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
const neg = num < 0;
|
||||
if (neg) num = -num;
|
||||
if (num < 1) return (neg ? "-" : "") + num + " B";
|
||||
const exponent = Math.min(
|
||||
Math.floor(Math.log(num) / Math.log(1000)),
|
||||
units.length - 1
|
||||
);
|
||||
const unit = units[exponent];
|
||||
num = Number((num / Math.pow(1000, exponent)).toFixed(2));
|
||||
return (neg ? "-" : "") + num + " " + unit;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## More Documentation
|
||||
|
||||
Check out the [API Documentation](//webtorrent.io/docs) and [FAQ](//webtorrent.io/faq) for more details.
|
||||
|
||||
[drag-drop]: https://npmjs.com/package/drag-drop
|
@@ -1,134 +0,0 @@
|
||||
# WebTorrent Tutorials
|
||||
|
||||
## Integrate WebTorrent with Video Players
|
||||
|
||||
WebTorrent can be used to stream videos. WebTorrent can render the incoming video to an HTML `<video>` element. Below are some examples for various video players.
|
||||
|
||||
### [Service Worker Renderer](https://github.com/webtorrent/webtorrent/blob/master/docs/api.md#clientloadworkercontroller-function-callback-controller---browser-only)
|
||||
|
||||
Code example:
|
||||
|
||||
```js
|
||||
import WebTorrent from "https://esm.sh/webtorrent";
|
||||
|
||||
const client = new WebTorrent();
|
||||
const torrentId =
|
||||
"magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent";
|
||||
const player = document.querySelector("video");
|
||||
|
||||
function download() {
|
||||
client.add(torrentId, (torrent) => {
|
||||
// Torrents can contain many files. Let's use the .mp4 file
|
||||
const file = torrent.files.find((file) => file.name.endsWith(".mp4"));
|
||||
// Log streams emitted by the video player
|
||||
file.on("stream", ({ stream, file, req }) => {
|
||||
if (req.destination === "video") {
|
||||
console.log(
|
||||
`Video player requested data from ${file.name}! Ranges: ${req.headers.range}`
|
||||
);
|
||||
}
|
||||
});
|
||||
// Stream to a <video> element by providing an the DOM element
|
||||
file.streamTo(player);
|
||||
console.log("Ready to play!");
|
||||
});
|
||||
}
|
||||
navigator.serviceWorker.register("./sw.min.js", { scope: "./" }).then((reg) => {
|
||||
const worker = reg.active || reg.waiting || reg.installing;
|
||||
function checkState(worker) {
|
||||
return (
|
||||
worker.state === "activated" &&
|
||||
client.createServer({ controller: reg }) &&
|
||||
download()
|
||||
);
|
||||
}
|
||||
if (!checkState(worker)) {
|
||||
worker.addEventListener("statechange", ({ target }) => checkState(target));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### [Video.js](https://videojs.com/)
|
||||
|
||||
Video.js is an open source HTML5 & Flash video player. We include the dependencies for `video.js` using CDN. A normal `<video>` element is converted to `video.js` by passing `class="video-js"` and `data-setup="{}"`. For more information visit the [docs](https://docs.videojs.com/tutorial-setup.html).
|
||||
|
||||
**Note**: Unlike in the Default HTML5 Video Player example we don't directly pass the ID of the `<video>` element but pass `` `video#${id}_html5_api` `` (JS String Literal). It is because `video.js` wraps the `<video>` element in a `<div>`.
|
||||
|
||||
Original code:
|
||||
|
||||
```html
|
||||
<video
|
||||
id="video-container"
|
||||
class="video-js"
|
||||
data-setup="{}"
|
||||
controls="true"
|
||||
></video>
|
||||
```
|
||||
|
||||
Code rendered on the browser:
|
||||
|
||||
```html
|
||||
<div>
|
||||
<video id="video-container_html5_api"></video>
|
||||
</div>
|
||||
```
|
||||
|
||||
Code example:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Web Torrent Tutorial</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdnjs.cloudflare.com/ajax/libs/video.js/7.8.1/video-js.min.css"
|
||||
/>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/video.js/7.8.1/video.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="video-container"
|
||||
class="video-js"
|
||||
data-setup="{}"
|
||||
controls="true"
|
||||
></video>
|
||||
<script type="module">
|
||||
import WebTorrent from "https://esm.sh/webtorrent";
|
||||
const client = new WebTorrent();
|
||||
const torrentId =
|
||||
"magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent";
|
||||
const player = document.querySelector("video");
|
||||
|
||||
function download() {
|
||||
client.add(torrentId, (torrent) => {
|
||||
// Torrents can contain many files. Let's use the .mp4 file
|
||||
const file = torrent.files.find((file) => file.name.endsWith(".mp4"));
|
||||
|
||||
// Stream to a <video> element by providing an the DOM element
|
||||
file.streamTo(player);
|
||||
console.log("Ready to play!");
|
||||
});
|
||||
}
|
||||
navigator.serviceWorker
|
||||
.register("./sw.min.js", { scope: "./" })
|
||||
.then((reg) => {
|
||||
const worker = reg.active || reg.waiting || reg.installing;
|
||||
function checkState(worker) {
|
||||
return (
|
||||
worker.state === "activated" &&
|
||||
client.createServer({ controller: reg }) &&
|
||||
download()
|
||||
);
|
||||
}
|
||||
if (!checkState(worker)) {
|
||||
worker.addEventListener("statechange", ({ target }) =>
|
||||
checkState(target)
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
Reference in New Issue
Block a user