Youtube to m3u8 (Cloudflare worker)

You can now convert all youtube livestream to hls m3u8 format
paste the worker script and deploy , find youtube video id;

example: 1754898358258.webp

format: You do not have permission to view the full content of this post. Log in or register now.You do not have permission to view the full content of this post. Log in or register now.stream.m3u8 or /channelID/stream.m3u8
channel ID must have "@" example: @ptvph, @gmanetwork



Code:
const USER_AGENT =
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
  "(KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36";
let activeRequests = 0;

// List of alternative YouTube endpoints to try if primary fails
const YOUTUBE_ENDPOINTS = [
  "https://www.youtube.com",
  "https://youtube.com",
  "https://m.youtube.com",
  "https://www.youtube.com/embed",
  "https://www.youtube-nocookie.com"
];

addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  try {
    activeRequests++;
    console.log(`Active requests: ${activeRequests}`);
    const url = new URL(request.url);
    const parts = url.pathname.split("/").filter(Boolean);
    const qp = url.searchParams;
   
    // Log the request for debugging
    console.log(`Processing request for: ${url.pathname}`);
   
    if (parts.length < 2) {
      return new Response("Usage: /@handle/stream.m3u8", { status: 400 });
    }
   
    const handle = parts[0]; // @ptvph or channel id
    const filename = parts[1];
   
    if (!filename.endsWith(".m3u8")) {
      return new Response("Only .m3u8 supported in this Worker", { status: 400 });
    }
   
    // 1) Segment proxy: ?url=<encoded>
    if (qp.has("url")) {
      const target = qp.get("url");
      return await handleSegmentProxy(target, request);
    }
   
    // 2) Variant request: ?variant=<encoded_variant_m3u8_url>
    if (qp.has("variant")) {
      const variantUrl = qp.get("variant");
      return await handleVariantPlaylistProxy(variantUrl, request);
    }
   
    // 3) Master request: fetch youtube page -> get hlsManifestUrl -> fetch master -> rewrite and return
    return await handleMasterRequest(handle, request);
  } catch (err) {
    console.error("Worker error:", err);
    return new Response("Worker error: " + String(err.message || err), {
      status: 500,
      headers: textHeaders(),
    });
  } finally {
    activeRequests--;
    console.log(`Active requests: ${activeRequests}`);
  }
}

/* -------------------------
   Helper functions & steps
   ------------------------- */
function textHeaders() {
  return {
    "Content-Type": "text/plain; charset=utf-8",
    "Access-Control-Allow-Origin": "*",
    "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
  };
}

function corsHeaders(additional = {}) {
  const h = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "Range,Accept,Content-Type",
    "Access-Control-Expose-Headers": "Content-Length,Content-Range",
    "Access-Control-Allow-Methods": "GET,HEAD,OPTIONS",
    "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
    ...additional,
  };
  return h;
}

async function fetchWithUA(resource, init = {}) {
  init.headers = Object.assign({
    "User-Agent": USER_AGENT,
    "Accept": "*/*",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "DNT": "1",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1"
  }, init.headers || {});
 
  const controller = new AbortController();
  init.signal = controller.signal;
  const timeoutMs = init.timeoutMs || 15000;
  const timer = setTimeout(() => controller.abort(), timeoutMs);
 
  try {
    console.log(`Fetching: ${resource}`);
    const r = await fetch(resource, init);
    clearTimeout(timer);
    return r;
  } catch (e) {
    clearTimeout(timer);
    console.error(`Fetch failed for ${resource}:`, e);
    throw e;
  }
}

/* ---- Master: fetch youtube live page & return rewritten master playlist ---- */
async function handleMasterRequest(handle, request) {
  let manifestUrl;
  let lastError;
 
  // Try different approaches to get the manifest URL
  const approaches = [
    // Try with @handle format
    () => extractHlsManifestFromYouTube(`https://www.youtube.com/${handle}/live`),
    // Try with channel ID format
    () => extractHlsManifestFromYouTube(`https://www.youtube.com/channel/${handle}/live`),
    // Try with video ID format
    () => extractHlsManifestFromYouTube(`https://www.youtube.com/watch?v=${handle}`),
    // Try with embed format
    () => extractHlsManifestFromYouTube(`https://www.youtube.com/embed/${handle}`),
    // Try with mobile format
    () => extractHlsManifestFromYouTube(`https://m.youtube.com/watch?v=${handle}`),
    // Try alternative endpoints
    ...YOUTUBE_ENDPOINTS.slice(1).map(endpoint => {
      return () => {
        const url = handle.startsWith("@")
          ? `${endpoint}/${handle}/live`
          : `${endpoint}/watch?v=${handle}`;
        return extractHlsManifestFromYouTube(url);
      };
    })
  ];
 
  // Try each approach until one works
  for (const approach of approaches) {
    try {
      manifestUrl = await approach();
      if (manifestUrl) {
        console.log(`Found manifest URL: ${manifestUrl}`);
        break;
      }
    } catch (e) {
      lastError = e;
      console.warn(`Approach failed: ${e.message}`);
    }
  }
 
  if (!manifestUrl) {
    console.error("All approaches failed to find manifest URL");
    return new Response(`Could not find hlsManifestUrl for this handle/id. Last error: ${lastError?.message || 'Unknown error'}`, {
      status: 404,
      headers: textHeaders(),
    });
  }
 
  try {
    const masterRes = await fetchWithUA(manifestUrl, { method: "GET" });
    if (!masterRes.ok) {
      throw new Error(`Upstream manifest returned ${masterRes.status}`);
    }
   
    const masterTxt = await masterRes.text();
    const encrypted = /#EXT-X-KEY/i.test(masterTxt);
    const proxyBase = request.url.split("?")[0];
   
    // Rewrite URLs in the manifest to go through our proxy
    const rewritten = masterTxt.replace(/(https?:\/\/[^\s\r\n,]+)/g, (m) => {
      if (/\.m3u8(\?|$)/i.test(m)) {
        return `${proxyBase}?variant=${encodeURIComponent(m)}`;
      }
      return `${proxyBase}?variant=${encodeURIComponent(m)}`;
    });
   
    const headers = {
      "Content-Type": "application/vnd.apple.mpegurl",
      "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
      ...corsHeaders(),
    };
   
    const debug = new URL(request.url).searchParams.get("debug");
    if (debug) {
      const dbg = `# Worker debugging\n# source_manifest: ${manifestUrl}\n# master_encrypted: ${encrypted}\n\n${masterTxt}\n\n--- rewritten ---\n\n${rewritten}`;
      return new Response(dbg, {
        status: 200,
        headers: { "Content-Type": "text/plain; charset=utf-8", ...corsHeaders() }
      });
    }
   
    return new Response(rewritten, { status: 200, headers });
  } catch (e) {
    console.error("Error processing manifest:", e);
    return new Response(`Error processing manifest: ${e.message}`, {
      status: 502,
      headers: textHeaders(),
    });
  }
}

/* ---- Variant playlist proxy: fetch variant m3u8 fresh and rewrite segments ---- */
async function handleVariantPlaylistProxy(variantUrl, request) {
  if (!variantUrl || !/^https?:\/\//i.test(variantUrl)) {
    return new Response("Invalid variant URL", { status: 400, headers: textHeaders() });
  }
 
  try {
    const res = await fetchWithUA(variantUrl, { method: "GET" });
    if (!res.ok) {
      throw new Error(`Upstream variant returned ${res.status}`);
    }
   
    const txt = await res.text();
    const encrypted = /#EXT-X-KEY/i.test(txt);
    const proxyBase = request.url.split("?")[0];
   
    const rewritten = txt
      .split(/\r?\n/)
      .map((line) => {
        if (!line || line.startsWith("#")) return line;
        try {
          const resolved = new URL(line, variantUrl).toString();
          return `${proxyBase}?url=${encodeURIComponent(resolved)}`;
        } catch (e) {
          console.warn(`Failed to resolve URL: ${line}`, e);
          return line;
        }
      })
      .join("\n");
   
    const headers = {
      "Content-Type": "application/vnd.apple.mpegurl",
      "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
      ...corsHeaders(),
    };
   
    const debug = new URL(request.url).searchParams.get("debug");
    if (debug) {
      const dbg = `# Worker debugging\n# variant_source: ${variantUrl}\n# encrypted: ${encrypted}\n\n--- original variant ---\n\n${txt}\n\n--- rewritten variant ---\n\n${rewritten}`;
      return new Response(dbg, {
        status: 200,
        headers: { "Content-Type": "text/plain; charset=utf-8", ...corsHeaders() }
      });
    }
   
    return new Response(rewritten, { status: 200, headers });
  } catch (e) {
    console.error("Error processing variant playlist:", e);
    return new Response(`Error processing variant playlist: ${e.message}`, {
      status: 502,
      headers: textHeaders(),
    });
  }
}

/* ---- Segment proxy: fetch bytes from the target segment URL and return them ---- */
async function handleSegmentProxy(targetUrl, request) {
  if (!targetUrl || !/^https?:\/\//i.test(targetUrl)) {
    return new Response("Invalid segment URL", { status: 400, headers: textHeaders() });
  }
 
  const range = request.headers.get("range");
  const headers = {};
  if (range) headers.Range = range;
 
  let lastErr = null;
 
  // Try multiple times to fetch the segment
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      const upstream = await fetchWithUA(targetUrl, {
        method: "GET",
        headers,
        redirect: "follow",
        timeoutMs: 20000
      });
     
      if (upstream.status === 403 || upstream.status === 401) {
        throw new Error(`Upstream blocked (${upstream.status})`);
      }
     
      if (!upstream.ok) {
        throw new Error(`Upstream returned ${upstream.status}`);
      }
     
      const contentType = upstream.headers.get("Content-Type") || guessContentTypeFromUrl(targetUrl);
      const contentLength = upstream.headers.get("Content-Length");
      const contentRange = upstream.headers.get("Content-Range");
     
      const outHeaders = new Headers();
      outHeaders.set("Content-Type", contentType);
      if (contentLength) outHeaders.set("Content-Length", contentLength);
      if (contentRange) outHeaders.set("Content-Range", contentRange);
      outHeaders.set("Accept-Ranges", upstream.headers.get("Accept-Ranges") || "bytes");
      outHeaders.set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
      outHeaders.set("Access-Control-Allow-Origin", "*");
      outHeaders.set("Access-Control-Expose-Headers", "Content-Length,Content-Range");
      outHeaders.set("Vary", "Origin");
     
      return new Response(upstream.body, { status: upstream.status, headers: outHeaders });
    } catch (err) {
      lastErr = err;
      console.warn(`Segment fetch attempt ${attempt + 1} failed:`, err);
      await new Promise((r) => setTimeout(r, 300 * (attempt + 1)));
    }
  }
 
  console.error("All segment fetch attempts failed");
  return new Response("Failed to fetch segment: " + String(lastErr), {
    status: 502,
    headers: textHeaders()
  });
}

/* ---- Utility: get hlsManifestUrl from youtube page ---- */
async function extractHlsManifestFromYouTube(youtubePageUrl) {
  try {
    const res = await fetchWithUA(youtubePageUrl, { method: "GET" });
    if (!res.ok) throw new Error(`YouTube returned ${res.status}`);
   
    const text = await res.text();
   
    // Try multiple regex patterns to find the manifest URL
    const patterns = [
      /(?<=hlsManifestUrl":")[^"]+\.m3u8/g,
      /"hlsManifestUrl"\s*:\s*"([^"]+\.m3u8)"/,
      /https?:\/\/[^"']+\.m3u8[^"']*/,
      /"url"\s*:\s*"([^"]+\.m3u8[^"]*)"/,
      /"streamingData"\s*:\s*({[^}]+})/
    ];
   
    for (const pattern of patterns) {
      let m = text.match(pattern);
      if (m && m[0]) {
        let manifestUrl = m[0];
        // If it's a streamingData object, try to parse it
        if (pattern === patterns[4]) {
          try {
            const streamingData = JSON.parse(manifestUrl);
            if (streamingData.hlsManifestUrl) {
              manifestUrl = streamingData.hlsManifestUrl;
            }
          } catch (e) {
            continue;
          }
        }
       
        // Clean up the URL
        manifestUrl = manifestUrl.replace(/\\u0026/g, "&");
        manifestUrl = decodeURIComponent(manifestUrl.replace(/\&amp;/g, "&"));
       
        if (manifestUrl.startsWith("http")) {
          return manifestUrl;
        }
      }
    }
   
    throw new Error("hlsManifestUrl not found in YouTube page HTML");
  } catch (e) {
    console.error(`Failed to extract manifest from ${youtubePageUrl}:`, e);
    throw e;
  }
}

function guessContentTypeFromUrl(u) {
  if (/\.(m3u8)(\?|$)/i.test(u)) return "application/vnd.apple.mpegurl";
  if (/\.(ts)(\?|$)/i.test(u)) return "video/mp2t";
  if (/\.(mp4)(\?|$)/i.test(u)) return "video/mp4";
  return "application/octet-stream";
}




step to play this in hls format
step 1: if you don't have account in cloudflare make an account first in You do not have permission to view the full content of this post. Log in or register now.
step 2: after making an account , select " compute workers"
step 3: click create
step 4: select START WITH HELLO WORLD and click GET STARTED

1754916597773.webp
rename the worker
step 5: click deploy
paste the worker script and click deploy and test your worker now

pa feedback kung if worker sainyo . I've been using this script for a week now . sharing is caring :)
 
TS, paturo po, di po ako programmer.

ganito po ba siya?

View attachment 3862550


View attachment 3862549


sinalpak ko po sa vlc, di po nag play, ano po kaya mali ko?

View attachment 3862551

thanks!
Hello sir,

ginawa ko na po yung sa cloudfare.

1754964019286.webp

na edit paste ko na rin po ang script.

tapos yan na po siya, may pipindutin po ba dito para po maka convert po ako?

kasi po sinalpak ko po ulit ang livestream ID direct sa VLC po, hindi po ulit nagplay.

baka po may tip kayo pag nandito na po.

thanks Ts!



TS, mukhang napagana ko na po - ganon po pala yun....


yung example url ninyo papalitan ko nung actual na na generate nung cloudfare entry ko po -

heto na siya TS o. Astig po! thanks TS and more power!

1754965222116.webp
 
Hello sir,

ginawa ko na po yung sa cloudfare.

View attachment 3863272

na edit paste ko na rin po ang script.

tapos yan na po siya, may pipindutin po ba dito para po maka convert po ako?

kasi po sinalpak ko po ulit ang livestream ID direct sa VLC po, hindi po ulit nagplay.

baka po may tip kayo pag nandito na po.

thanks Ts!
dito mo ipapaste ung script
1754965032499.webppaste mo ung script dito , burahin mo ung hello world jan

tapos click mo ung deploy after mo ma lagay ung script

tapos ang format ng script dapat

You do not have permission to view the full content of this post. Log in or register now.
 

About this Thread

  • 80
    Replies
  • 4K
    Views
  • 40
    Participants
Last reply from:
johndkl

Trending Topics

Online now

Members online
1,188
Guests online
3,913
Total visitors
5,101

Forum statistics

Threads
2,322,465
Posts
29,215,502
Members
1,175,529
Latest member
estanolreyniel777
Back
Top