mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-09 23:48:44 +00:00
update
This commit is contained in:
341
src copy/js/app.js
Normal file
341
src copy/js/app.js
Normal file
@@ -0,0 +1,341 @@
|
||||
// js/app.js
|
||||
|
||||
import { nostrClient } from './nostr.js';
|
||||
import { torrentClient } from './webtorrent.js';
|
||||
import { isDevMode } from './config.js';
|
||||
|
||||
class NosTubeApp {
|
||||
constructor() {
|
||||
this.loginButton = document.getElementById('loginButton');
|
||||
this.logoutButton = document.getElementById('logoutButton'); // Added logout button
|
||||
this.userStatus = document.getElementById('userStatus');
|
||||
this.userPubKey = document.getElementById('userPubKey');
|
||||
this.submitForm = document.getElementById('submitForm');
|
||||
this.playerModal = document.getElementById('playerModal');
|
||||
this.player = document.getElementById('player');
|
||||
this.videoList = document.getElementById('videoList');
|
||||
this.closePlayerBtn = document.getElementById('closePlayer');
|
||||
this.errorContainer = document.getElementById('errorContainer');
|
||||
this.successContainer = document.getElementById('successContainer');
|
||||
this.videoFormContainer = document.getElementById('videoFormContainer'); // Added form container
|
||||
|
||||
this.pubkey = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the application by setting up the Nostr client and loading videos.
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
// Ensure the modal is hidden by default
|
||||
this.playerModal.classList.add('hidden');
|
||||
|
||||
// Hide the video submission form initially
|
||||
this.videoFormContainer.classList.add('hidden');
|
||||
|
||||
// Initialize Nostr client
|
||||
await nostrClient.init();
|
||||
console.log('Nostr client initialized.');
|
||||
|
||||
// Check if user is already logged in (e.g., from localStorage)
|
||||
const savedPubKey = localStorage.getItem('userPubKey');
|
||||
if (savedPubKey) {
|
||||
this.login(savedPubKey, false); // Do not prompt for login again
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
console.log('Event listeners set up.');
|
||||
|
||||
// Load videos
|
||||
await this.loadVideos();
|
||||
console.log('Videos loaded.');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize app:', error);
|
||||
this.showError('Failed to connect to Nostr relay. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners for login, logout, form submission, and modal interactions.
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Login Button
|
||||
this.loginButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const pubkey = await nostrClient.login();
|
||||
this.login(pubkey, true);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
this.showError('Failed to login. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
// Logout Button
|
||||
this.logoutButton.addEventListener('click', () => {
|
||||
this.logout();
|
||||
});
|
||||
|
||||
// Form submission
|
||||
this.submitForm.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||
|
||||
// Close player modal
|
||||
if (this.closePlayerBtn) {
|
||||
this.closePlayerBtn.addEventListener('click', () => {
|
||||
console.log('Close button clicked. Hiding modal...');
|
||||
this.hideModal();
|
||||
});
|
||||
} else {
|
||||
console.error('Close button not found!');
|
||||
}
|
||||
|
||||
// Close modal when clicking outside the modal content
|
||||
if (this.playerModal) {
|
||||
this.playerModal.addEventListener('click', (e) => {
|
||||
if (e.target === this.playerModal) {
|
||||
console.log('Clicked outside modal content. Hiding modal...');
|
||||
this.hideModal();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('playerModal not found!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user login by updating UI elements.
|
||||
* @param {string} pubkey - The public key of the logged-in user.
|
||||
* @param {boolean} saveToStorage - Whether to save the pubkey to localStorage.
|
||||
*/
|
||||
login(pubkey, saveToStorage = true) {
|
||||
this.pubkey = pubkey;
|
||||
this.loginButton.classList.add('hidden');
|
||||
this.logoutButton.classList.remove('hidden');
|
||||
this.userStatus.classList.remove('hidden');
|
||||
this.userPubKey.textContent = pubkey;
|
||||
this.videoFormContainer.classList.remove('hidden'); // Show form
|
||||
console.log(`User logged in as: ${pubkey}`);
|
||||
|
||||
if (saveToStorage) {
|
||||
localStorage.setItem('userPubKey', pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the user by resetting UI elements and internal state.
|
||||
*/
|
||||
logout() {
|
||||
nostrClient.logout();
|
||||
this.pubkey = null;
|
||||
this.loginButton.classList.remove('hidden');
|
||||
this.logoutButton.classList.add('hidden');
|
||||
this.userStatus.classList.add('hidden');
|
||||
this.userPubKey.textContent = '';
|
||||
this.videoFormContainer.classList.add('hidden'); // Hide form
|
||||
localStorage.removeItem('userPubKey');
|
||||
console.log('User logged out.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the player modal, clears the player content, and stops streaming.
|
||||
*/
|
||||
async hideModal() {
|
||||
if (this.playerModal) {
|
||||
this.playerModal.classList.add('hidden');
|
||||
this.playerModal.classList.remove('flex');
|
||||
console.log('Modal hidden.');
|
||||
} else {
|
||||
console.error('playerModal is undefined.');
|
||||
}
|
||||
|
||||
if (this.player) {
|
||||
this.player.innerHTML = ''; // Clear video content when modal is closed
|
||||
console.log('Player content cleared.');
|
||||
} else {
|
||||
console.error('player is undefined.');
|
||||
}
|
||||
|
||||
try {
|
||||
await torrentClient.stopStreaming();
|
||||
console.log('Streaming stopped.');
|
||||
} catch (error) {
|
||||
console.error('Error stopping streaming:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submission of a new video.
|
||||
* @param {Event} e - The form submission event.
|
||||
*/
|
||||
async handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.pubkey) {
|
||||
this.showError('Please login to post a video.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
title: document.getElementById('title').value.trim(),
|
||||
magnet: document.getElementById('magnet').value.trim(),
|
||||
thumbnail: document.getElementById('thumbnail').value.trim(),
|
||||
mode: isDevMode ? 'dev' : 'live', // Add mode to the metadata
|
||||
};
|
||||
|
||||
// Basic client-side validation
|
||||
if (!formData.title || !formData.magnet) {
|
||||
this.showError('Title and Magnet URI are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await nostrClient.publishVideo(formData, this.pubkey);
|
||||
this.submitForm.reset();
|
||||
await this.loadVideos(); // Refresh video list
|
||||
this.showSuccess('Video shared successfully!');
|
||||
} catch (error) {
|
||||
console.error('Failed to publish video:', error.message);
|
||||
this.showError('Failed to share video. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads videos from the relays and renders them.
|
||||
*/
|
||||
async loadVideos() {
|
||||
try {
|
||||
const videos = await nostrClient.fetchVideos();
|
||||
if (videos.length === 0) {
|
||||
console.log('No valid videos found.');
|
||||
}
|
||||
this.renderVideoList(videos);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch videos:', error.message);
|
||||
this.showError('An error occurred while loading videos. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the list of videos in the UI.
|
||||
* @param {Array} videos - An array of video objects to render.
|
||||
*/
|
||||
renderVideoList(videos) {
|
||||
if (videos.length === 0) {
|
||||
this.videoList.innerHTML = '<p class="text-center text-gray-500">No videos available yet. Be the first to upload one!</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort videos by creation date (newest first)
|
||||
videos.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
this.videoList.innerHTML = videos.map(video => `
|
||||
<div class="video-card ${video.mode === 'dev' ? 'border border-red-500' : ''}">
|
||||
<div class="aspect-w-16 aspect-h-9 bg-gray-100">
|
||||
${video.thumbnail ?
|
||||
`<img src="${this.escapeHTML(video.thumbnail)}" alt="${this.escapeHTML(video.title)}" class="object-cover w-full h-48">` :
|
||||
'<div class="flex items-center justify-center h-48 bg-gray-200">No thumbnail</div>'
|
||||
}
|
||||
</div>
|
||||
<div class="details p-4">
|
||||
<h3 class="text-lg font-semibold mb-2">${this.escapeHTML(video.title)}</h3>
|
||||
<p class="text-sm ${video.mode === 'dev' ? 'text-red-500' : 'text-green-500'}">
|
||||
${video.mode.toUpperCase()}
|
||||
</p>
|
||||
<button
|
||||
onclick="app.playVideo('${encodeURIComponent(video.magnet)}')"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Play Video
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the selected video using the torrent client.
|
||||
* @param {string} magnetURI - The Magnet URI of the video to play.
|
||||
*/
|
||||
async playVideo(magnetURI) {
|
||||
if (!magnetURI) {
|
||||
this.showError('Invalid Magnet URI.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Opening video modal...');
|
||||
this.playerModal.classList.remove('hidden');
|
||||
this.playerModal.classList.add('flex');
|
||||
console.log('Modal opened for video playback.');
|
||||
|
||||
try {
|
||||
await torrentClient.streamVideo(decodeURIComponent(magnetURI), this.player);
|
||||
} catch (error) {
|
||||
console.error('Failed to play video:', error.message);
|
||||
this.showError('Failed to play video. Please try again.');
|
||||
this.hideModal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message to the user.
|
||||
* @param {string} message - The error message to display.
|
||||
*/
|
||||
showError(message) {
|
||||
if (this.errorContainer) {
|
||||
this.errorContainer.textContent = message;
|
||||
this.errorContainer.classList.remove('hidden');
|
||||
console.warn(`Error displayed to user: ${message}`);
|
||||
|
||||
// Hide the error message after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.errorContainer.classList.add('hidden');
|
||||
this.errorContainer.textContent = '';
|
||||
}, 5000);
|
||||
} else {
|
||||
console.warn('Error container not found. Falling back to alert.');
|
||||
alert(message); // Fallback for missing error container
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a success message to the user.
|
||||
* @param {string} message - The success message to display.
|
||||
*/
|
||||
showSuccess(message) {
|
||||
if (this.successContainer) {
|
||||
this.successContainer.textContent = message;
|
||||
this.successContainer.classList.remove('hidden');
|
||||
console.log(`Success message displayed: ${message}`);
|
||||
|
||||
// Hide the success message after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.successContainer.classList.add('hidden');
|
||||
this.successContainer.textContent = '';
|
||||
}, 5000);
|
||||
} else {
|
||||
console.log('Success container not found. Falling back to alert.');
|
||||
alert(message); // Fallback for missing success container
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes HTML characters to prevent XSS attacks.
|
||||
* @param {string} unsafe - The string to escape.
|
||||
* @returns {string} The escaped string.
|
||||
*/
|
||||
escapeHTML(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize app
|
||||
const app = new NosTubeApp();
|
||||
app.init();
|
||||
|
||||
// Make playVideo accessible globally for the onclick handlers
|
||||
window.app = app;
|
3
src copy/js/config.js
Normal file
3
src copy/js/config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// js/config.js
|
||||
|
||||
export const isDevMode = true; // Set to false for production
|
8676
src copy/js/libs/nostr.bundle.js
Normal file
8676
src copy/js/libs/nostr.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
227
src copy/js/nostr.js
Normal file
227
src copy/js/nostr.js
Normal file
@@ -0,0 +1,227 @@
|
||||
// js/nostr.js
|
||||
|
||||
import { isDevMode } from './config.js';
|
||||
|
||||
const RELAY_URLS = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.snort.social',
|
||||
'wss://nostr.wine'
|
||||
];
|
||||
|
||||
class NostrClient {
|
||||
constructor() {
|
||||
this.pool = new window.NostrTools.SimplePool(); // Access via window
|
||||
this.pubkey = null;
|
||||
this.relays = RELAY_URLS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Nostr client by connecting to relays.
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
console.log('Connecting to relays...');
|
||||
|
||||
// Test relay connections
|
||||
const testFilter = { kinds: [0], limit: 1 }; // Dummy filter for testing
|
||||
const connections = this.relays.map(async url => {
|
||||
try {
|
||||
const sub = this.pool.sub([url], [testFilter]);
|
||||
sub.on('event', event => console.log(`Test event from ${url}:`, event));
|
||||
sub.on('eose', () => {
|
||||
console.log(`Relay ${url} connected successfully.`);
|
||||
sub.unsub();
|
||||
});
|
||||
return { url, success: true };
|
||||
} catch (err) {
|
||||
console.error(`Failed to connect to relay: ${url}`, err.message);
|
||||
return { url, success: false };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(connections);
|
||||
const successfulRelays = results.filter(r => r.success).map(r => r.url);
|
||||
if (successfulRelays.length === 0) {
|
||||
throw new Error('No relays could be connected.');
|
||||
}
|
||||
|
||||
console.log(`Connected to ${successfulRelays.length} relay(s):`, successfulRelays);
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize Nostr client:', err.message);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in the user using a Nostr extension or by entering an NSEC key.
|
||||
* @returns {Promise<string>} The public key of the logged-in user.
|
||||
*/
|
||||
async login() {
|
||||
if (window.nostr) {
|
||||
try {
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
this.pubkey = pubkey;
|
||||
console.log('Logged in with extension. Public key:', this.pubkey);
|
||||
return this.pubkey;
|
||||
} catch (e) {
|
||||
console.warn('Failed to get public key from Nostr extension:', e.message);
|
||||
throw new Error('Failed to get public key from Nostr extension.');
|
||||
}
|
||||
} else {
|
||||
const nsec = prompt('Enter your NSEC key:');
|
||||
if (nsec) {
|
||||
try {
|
||||
this.pubkey = this.decodeNsec(nsec);
|
||||
console.log('Logged in with NSEC. Public key:', this.pubkey);
|
||||
return this.pubkey;
|
||||
} catch (error) {
|
||||
console.error('Invalid NSEC key:', error.message);
|
||||
throw new Error('Invalid NSEC key.');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Login cancelled or NSEC key not provided.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the user by clearing the public key.
|
||||
*/
|
||||
logout() {
|
||||
this.pubkey = null;
|
||||
console.log('User logged out.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes an NSEC key to retrieve the public key.
|
||||
* @param {string} nsec - The NSEC key.
|
||||
* @returns {string} The corresponding public key.
|
||||
*/
|
||||
decodeNsec(nsec) {
|
||||
try {
|
||||
const { data } = window.NostrTools.nip19.decode(nsec); // Access via window
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new Error('Invalid NSEC key.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes a new video event to all relays.
|
||||
* @param {Object} videoData - The video data to publish.
|
||||
* @param {string} pubkey - The public key of the user publishing the video.
|
||||
* @returns {Promise<Object>} The signed event.
|
||||
*/
|
||||
async publishVideo(videoData, pubkey) {
|
||||
if (!pubkey) {
|
||||
throw new Error('User is not logged in.');
|
||||
}
|
||||
|
||||
const event = {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['t', 'video']],
|
||||
content: JSON.stringify(videoData)
|
||||
};
|
||||
|
||||
try {
|
||||
const signedEvent = await window.nostr.signEvent(event);
|
||||
await Promise.all(this.relays.map(async url => {
|
||||
try {
|
||||
await this.pool.publish([url], signedEvent);
|
||||
console.log(`Event published to ${url}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to publish to ${url}:`, err.message);
|
||||
}
|
||||
}));
|
||||
return signedEvent;
|
||||
} catch (error) {
|
||||
console.error('Failed to sign event:', error.message);
|
||||
throw new Error('Failed to sign event.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches videos from all configured relays, ensuring that only valid video events are included.
|
||||
* Filters videos based on the current mode (dev or live).
|
||||
* @returns {Promise<Array>} An array of valid video objects.
|
||||
*/
|
||||
async fetchVideos() {
|
||||
const filter = {
|
||||
kinds: [30078], // Video kind
|
||||
limit: 50 // Fetch up to 50 videos
|
||||
};
|
||||
const videos = new Map(); // Use a Map to ensure unique videos by event ID
|
||||
|
||||
// Initialize summary counters
|
||||
const invalidEventSummary = {
|
||||
malformedJson: 0,
|
||||
invalidFormat: 0
|
||||
};
|
||||
|
||||
// Fetch videos from all relays
|
||||
await Promise.all(
|
||||
this.relays.map(async url => {
|
||||
try {
|
||||
const events = await this.pool.list([url], [filter]);
|
||||
events.forEach(event => {
|
||||
try {
|
||||
const content = JSON.parse(event.content);
|
||||
// Filter by mode
|
||||
if (content.mode === (isDevMode ? 'dev' : 'live') && this.isValidVideo(content)) {
|
||||
if (!videos.has(event.id)) { // Ensure uniqueness
|
||||
videos.set(event.id, {
|
||||
id: event.id,
|
||||
title: content.title,
|
||||
magnet: content.magnet,
|
||||
thumbnail: content.thumbnail || '',
|
||||
mode: content.mode,
|
||||
pubkey: event.pubkey,
|
||||
created_at: event.created_at,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (jsonError) {
|
||||
invalidEventSummary.malformedJson++;
|
||||
console.error(
|
||||
`Failed to parse video content from ${url}: ${jsonError.message} | Event ID: ${event.id}`
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (relayError) {
|
||||
console.error(`Failed to fetch videos from relay ${url}: ${relayError.message}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Log a summary of issues
|
||||
if (invalidEventSummary.malformedJson > 0) {
|
||||
console.warn(`Skipped ${invalidEventSummary.malformedJson} event(s) due to malformed JSON.`);
|
||||
}
|
||||
if (invalidEventSummary.invalidFormat > 0) {
|
||||
console.warn(`Skipped ${invalidEventSummary.invalidFormat} event(s) due to invalid format.`);
|
||||
}
|
||||
|
||||
return Array.from(videos.values()); // Return unique videos as an array
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the structure of a video content object.
|
||||
* @param {Object} content - The content object to validate.
|
||||
* @returns {boolean} True if valid, false otherwise.
|
||||
*/
|
||||
isValidVideo(content) {
|
||||
return (
|
||||
typeof content === 'object' &&
|
||||
typeof content.title === 'string' &&
|
||||
typeof content.magnet === 'string' &&
|
||||
typeof content.mode === 'string' &&
|
||||
(typeof content.thumbnail === 'string' || typeof content.thumbnail === 'undefined')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Export the client
|
||||
export const nostrClient = new NostrClient();
|
122
src copy/js/webtorrent.js
Normal file
122
src copy/js/webtorrent.js
Normal file
@@ -0,0 +1,122 @@
|
||||
// js/webtorrent.js
|
||||
|
||||
export class TorrentClient {
|
||||
constructor() {
|
||||
this.client = new WebTorrent();
|
||||
this.currentTorrent = null;
|
||||
|
||||
// Handle client-level errors
|
||||
this.client.on('error', (err) => {
|
||||
console.error('WebTorrent client error:', err.message);
|
||||
// Optionally, emit events or handle errors globally
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams a video from a given Magnet URI into a specified HTML element.
|
||||
* @param {string} magnetURI - The Magnet URI of the torrent.
|
||||
* @param {HTMLElement} playerElement - The HTML element where the video will be rendered.
|
||||
* @returns {Promise<void>} Resolves when streaming starts successfully.
|
||||
*/
|
||||
streamVideo(magnetURI, playerElement) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!magnetURI) {
|
||||
reject(new Error('Magnet URI is required.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Adding torrent: ${magnetURI}`);
|
||||
|
||||
// If there's an existing torrent, remove it first
|
||||
if (this.currentTorrent) {
|
||||
console.log('Removing existing torrent before adding a new one.');
|
||||
this.client.remove(this.currentTorrent, (err) => {
|
||||
if (err) {
|
||||
console.error('Error removing existing torrent:', err.message);
|
||||
// Proceed to add the new torrent even if removal fails
|
||||
}
|
||||
this._addTorrent(magnetURI, playerElement, resolve, reject);
|
||||
});
|
||||
} else {
|
||||
this._addTorrent(magnetURI, playerElement, resolve, reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a torrent and streams the video.
|
||||
* @private
|
||||
* @param {string} magnetURI - The Magnet URI of the torrent.
|
||||
* @param {HTMLElement} playerElement - The HTML element where the video will be rendered.
|
||||
* @param {Function} resolve - The resolve function of the Promise.
|
||||
* @param {Function} reject - The reject function of the Promise.
|
||||
*/
|
||||
_addTorrent(magnetURI, playerElement, resolve, reject) {
|
||||
this.client.add(magnetURI, (torrent) => {
|
||||
this.currentTorrent = torrent;
|
||||
console.log('Torrent metadata received:', torrent.infoHash);
|
||||
|
||||
// Find the first compatible video file in the torrent
|
||||
const file = torrent.files.find(file => {
|
||||
return file.name.endsWith('.mp4') ||
|
||||
file.name.endsWith('.webm') ||
|
||||
file.name.endsWith('.mkv');
|
||||
});
|
||||
|
||||
if (!file) {
|
||||
console.error('No compatible video file found in the torrent.');
|
||||
reject(new Error('No compatible video file found in the torrent.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Streaming file:', file.name);
|
||||
|
||||
// Use renderTo for better compatibility and simplicity
|
||||
file.renderTo(playerElement, { autoplay: true, controls: true }, (err, elem) => {
|
||||
if (err) {
|
||||
console.error('Error rendering video:', err);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Video rendered successfully.');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle torrent-specific errors
|
||||
this.client.on('torrent', (torrent) => {
|
||||
torrent.on('error', (err) => {
|
||||
console.error(`Torrent error (${torrent.infoHash}):`, err.message);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops streaming the current torrent and cleans up resources.
|
||||
* @returns {Promise<void>} Resolves when the torrent is successfully removed.
|
||||
*/
|
||||
stopStreaming() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.currentTorrent) {
|
||||
console.log('Removing current torrent:', this.currentTorrent.infoHash);
|
||||
this.client.remove(this.currentTorrent, (err) => {
|
||||
if (err) {
|
||||
console.error('Error removing torrent:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('Torrent removed successfully.');
|
||||
this.currentTorrent = null;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('No active torrent to stop.');
|
||||
resolve(); // Nothing to do
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance of TorrentClient
|
||||
export const torrentClient = new TorrentClient();
|
Reference in New Issue
Block a user