P2P Media Loader

repository·main·Indexed 23 days ago

https://github.com/novage/p2p-media-loader

A JavaScript library for peer-to-peer media delivery of HLS and MPEG-DASH streams using WebRTC. It enables the creation of hybrid CDN/P2P networks to reduce bandwidth costs and origin server load. The library supports Hls.js and Shaka Player engines and integrates with players such as Vidstack, Clappr, MediaElement, Plyr, DPlayer, OpenPlayerJS, and PlayerJS.

Tokens
27.8K
Snippets
41
Records
117
Agent score
82%

What's inside p2p-media-loader

  1. Overview of P2P Media Loader

    main
    P2P Media Loader is an open-source JavaScript library that enables peer-to-peer (P2P) media delivery using modern browser technologies like HTML5 video and WebRTC. It allows users watching the same HLS or MPEG-DASH streams (live or VOD) to share traffic in real time, creating a hybrid CDN/P2P mesh network. This reduces origin server load and CDN bandwidth costs while increasing overall network capacity.
  2. How the `claimPeer` hook handles peer deduplication

    main

    To prevent redundant connections when multiple WebTorrentClient instances discover the same remote peer, the client uses an injected claimPeer(peer_id) hook for aggressive deduplication:

    1. When receiving an Offer: The client calls claimPeer(peer_id) using the peer_id from the tracker JSON. If it returns false, the client ignores the message and does not create an RTCPeerConnection.
    2. When receiving an Answer: The client calls claimPeer(peer_id) using the peer_id revealed in the answer. If it returns false, the client immediately closes the pending RTCPeerConnection and discards the answer.

    Developers implementing a Peer Manager must ensure claimPeer correctly identifies if a peer_id is already being managed by another instance.

  3. Hand-off of WebRTC connections to the Peer Manager

    main

    The WebTorrentClient is responsible only for the signaling and connection establishment phase. Its lifecycle ends immediately after the WebRTC data channel is successfully opened.

    Upon successful connection, the client emits the peerConnected event with the following payload:

    • peerId: The remote peer's ID.
    • connection: The RTCPeerConnection instance.
    • channel: The RTCDataChannel instance.

    Responsibility Shift: Once the event is emitted, the WebTorrentClient drops its internal references. The consuming Peer Manager must take full responsibility for:

    1. Monitoring channel.onclose and connection.oniceconnectionstatechange to detect drops.
    2. Implementing the BitTorrent wire protocol over the data channel.
    3. Cleaning up internal state when the connection closes.
  4. How WebTorrentSocketPool manages connections

    main

    The WebTorrentSocketPool uses a reference counting mechanism to manage and reuse WebSocketClient connections for WebTorrent trackers. This prevents redundant socket connections by sharing a single connection per unique URL across multiple clients.

    Lifecycle Flow:

    1. Acquisition: When acquire(url) is called, the pool checks for an existing connection to that URL. If found, it increments the reference count. If not, it creates a new WebSocketClient and sets the count to 1.
    2. Release: Every acquisition provides a release callback. Calling this decrements the reference count.
    3. Disposal: When the reference count reaches 0, the pool automatically calls dispose on the socket and removes it from the pool.
  5. How WebTorrentManager manages peer uniqueness

    main

    The WebTorrentManager ensures that the same remote peer is not connected multiple times, even if discovered across multiple trackers simultaneously.

    It achieves this by checking its internal collections of connectingPeers and connectedPeers before allowing a new connection. If a connection attempt for a peer_id is already in progress or established, the manager rejects the new connection attempt from that specific tracker.

    Lifecycle Notes:

    • If a WebRTC connection fails (e.g., ICE gathering or data channel timeout), the manager automatically removes the peer from the connectingPeers collection to allow future reconnection attempts.
    • If a connection is established and then rejected by the upper layer, the upper layer must call the close() callback provided in the peerConnected payload to ensure the peer is removed from the internal collections.
  6. How P2P Media Loader works

    main

    The library operates by combining traditional HTTP(S) downloads with WebRTC-based peer sharing:

    1. Initial Playback: The library initially downloads media segments via HTTP(S) from the source CDN to ensure fast startup.
    2. Peer Discovery: It transmits media stream details and connection info (like ICE candidates) to WebTorrent trackers. These trackers return a list of other peers watching the same stream.
    3. P2P Swarming: The library connects to these peers to download segments from them while simultaneously sharing segments it has already downloaded.
    4. Hybrid Model: If no peers are available, it falls back to standard HTTP(S) downloads. Periodically, random peers in the swarm download new segments via HTTP(S) to distribute them to the rest of the P2P network.
  7. Understand WebSocketClient reconnection and binary behavior

    main

    The WebSocketClient follows two key architectural principles:

    1. Exponential Backoff & Jitter: To avoid overwhelming servers, reconnection delays increase exponentially. The delay is calculated using the formula:

      • baseDelay = min(initialDelay * 2^backoffCount, maxDelay)
      • jitter = baseDelay * jitterMultiplier
      • delay = max(0, baseDelay + random(-jitter, jitter))
    2. Binary Compatibility: The client sets binaryType = 'arraybuffer' on the underlying native WebSocket. This ensures efficient and compatible handling of arbitrary binary protocols.

  8. P2P Network components and requirements

    main

    To function, the P2P network relies on several components:

    • WebRTC Data Channels: Used to exchange data between peers.
    • Media Source Extensions (MSE) / Managed Media Source: Required by Hls.js and Shaka Player engines for playback.
    • STUN Servers: Used by WebRTC to gather ICE candidates. The library uses public STUN servers by default.
    • WebTorrent Trackers: Used for WebRTC signaling and creating peer swarms. The library uses public trackers (e.g., https://tracker.novage.com.ua/) by default, meaning no server-side software is required for simple use cases.
  9. WebRTC Connection Flow and ICE Gathering

    main

    The WebTorrentClient uses a non-trickle ICE approach where all ICE candidates are bundled into a single SDP before sending to the tracker.

    ICE Gathering Timeout

    To prevent indefinite stalling, the client enforces a 5-second timeout on ICE gathering. If gathering does not complete within 5 seconds, the client proceeds with whatever candidates have been gathered so far.

    Initiating Connections (Sending Offers)

    Offers are generated in parallel using Promise.allSettled to minimize latency.

    1. For each slot, a new RTCPeerConnection and RTCDataChannel are created.
    2. An SDP offer is created and ICE gathering is waited for.
    3. A random 20-character alphanumeric offer_id is generated.
    4. The connection is stored in an internal Map keyed by offer_id.
    5. A default offerTimeout of 50s is set. If no answer arrives, the connection is closed and removed to prevent memory leaks.

    Receiving Offers

    1. The client calls claimPeer(peer_id) to deduplicate.
    2. It creates an RTCPeerConnection, calls setRemoteDescription, generates an SDP answer, and waits for ICE gathering.
    3. Once the data channel opens, it emits the peerConnected event.

    Receiving Answers

    1. The client calls claimPeer(peer_id) to deduplicate.
    2. If claimPeer is false, the pending connection is closed and discarded.
    3. If true, it applies setRemoteDescription(answer) to the pending connection.
    4. Once the data channel opens, it emits the peerConnected event.
  10. Integrate P2P with DPlayer and Shaka Player

    main

    To use P2P with DPlayer using Shaka Player:

    1. Call ShakaP2PEngine.registerPlugins().
    2. Instantiate ShakaP2PEngine with your core configuration.
    3. In the DPlayer video.customType configuration (using a type like customHlsOrDash), manually create a shaka.Player instance, attach it to the video element, and call shakaP2PEngine.bindShakaPlayer(shakaPlayer) before loading the stream.
    <script type="module">
      import { ShakaP2PEngine } from "p2p-media-loader-shaka";
    
      const container = document.getElementById("container");
    
      ShakaP2PEngine.registerPlugins();
    
      const shakaP2PEngine = new ShakaP2PEngine({
        core: {
          swarmId: "Optional custom swarm ID for stream",
          // Other P2P Media Loader Core options
        },
      });
    
      const player = new DPlayer({
        container,
        video: {
          url: "",
          type: "customHlsOrDash",
          customType: {
            customHlsOrDash: (video) => {
              const shakaPlayer = new shaka.Player();
              void shakaPlayer.attach(video);
    
              shakaP2PEngine.bindShakaPlayer(shakaPlayer);
              void shakaPlayer.load(streamUrl);
            },
          },
        },
      });
    </script>
  11. Integrate P2P with a standalone Hls.js player (IIFE)

    main

    For legacy environments or Smart TVs that do not support ES modules, use the IIFE builds. The Hls.js P2P engine is exposed via the global window.p2pml.hlsjs namespace.

    To integrate, use HlsJsP2PEngine.injectMixin(window.Hls) to create a new Hls constructor that includes P2P capabilities. You can then instantiate this constructor with a configuration object containing p2p.core settings (such as swarmId).

    <script src="https://cdn.jsdelivr.net/npm/hls.js@~1/dist/hls.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/p2p-media-loader-hlsjs@latest/dist/p2p-media-loader-hlsjs.iife.min.js"></script>
    
    <script>
      document.addEventListener("DOMContentLoaded", function () {
        var videoElement = document.getElementById("video");
        var streamUrl = "https://example.com/stream.m3u8";
    
        if (Hls.isSupported()) {
          // Access the engine from the global p2pml object
          var HlsJsP2PEngine = window.p2pml.hlsjs.HlsJsP2PEngine;
    
          var HlsWithP2P = HlsJsP2PEngine.injectMixin(window.Hls);
          var hls = new HlsWithP2P({
            p2p: {
              core: {
                swarmId: "Optional custom swarm ID for stream",
                // Other P2P engine configuration parameters go here
              },
            },
          });
    
          hls.attachMedia(videoElement);
          hls.loadSource(streamUrl);
        }
      });
    </script>