Skip to main content

Overview

yt-dlp-ejs is a JavaScript solver library that extracts and executes YouTube’s player challenge functions. The library uses a three-stage architecture: preprocessing, solving, and bundling.

Preprocessing

Parse and transform YouTube’s player JavaScript using AST manipulation

Solving

Extract and execute signature and n-parameter functions

Bundling

Package solvers for multiple JavaScript runtimes

Core Components

Main Entry Point

The solver’s main entry point processes requests through a unified interface:
src/yt/solver/main.ts
export default function main(input: Input): Output {
  const preprocessedPlayer =
    input.type === "player"
      ? preprocessPlayer(input.player)
      : input.preprocessed_player;
  const solvers = getFromPrepared(preprocessedPlayer);

  const responses = input.requests.map((input): Response => {
    if (!isOneOf(input.type, "n", "sig")) {
      return {
        type: "error",
        error: `Unknown request type: ${input.type}`,
      };
    }
    const solver = solvers[input.type];
    if (!solver) {
      return {
        type: "error",
        error: `Failed to extract ${input.type} function`,
      };
    }
    try {
      return {
        type: "result",
        data: Object.fromEntries(
          input.challenges.map((challenge) => [challenge, solver(challenge)]),
        ),
      };
    } catch (error) {
      return {
        type: "error",
        error:
          error instanceof Error
            ? `${error.message}\n${error.stack}`
            : `${error}`,
      };
    }
  });

  const output: Output = {
    type: "result",
    responses,
  };
  if (input.type === "player" && input.output_preprocessed) {
    output.preprocessed_player = preprocessedPlayer;
  }
  return output;
}
The main function accepts either raw player JavaScript or preprocessed player code, enabling caching of the preprocessing step for improved performance.

Input/Output Types

The solver uses a strongly-typed interface:
type Input =
  | {
      type: "player";
      player: string;
      requests: Request[];
      output_preprocessed: boolean;
    }
  | {
      type: "preprocessed";
      preprocessed_player: string;
      requests: Request[];
    };

type Request = {
  type: "n" | "sig";
  challenges: string[];
};

type Response =
  | {
      type: "result";
      data: Record<string, string>;
    }
  | {
      type: "error";
      error: string;
    };

Preprocessing Pipeline

AST Manipulation with Meriyah and Astring

The preprocessing stage uses meriyah (JavaScript parser) and astring (code generator) to transform YouTube’s player code:
1

Parse Player JavaScript

Convert the raw player JavaScript into an Abstract Syntax Tree (AST) using meriyah
src/yt/solver/solvers.ts
export function preprocessPlayer(data: string): string {
  const program = parse(data);
  const plainStatements = modifyPlayer(program);
  // ...
}
2

Extract Statements

Navigate the AST to extract core statements from the player’s IIFE wrapper
export function modifyPlayer(program: ESTree.Program) {
  const body = program.body;
  const block: ESTree.BlockStatement = (() => {
    switch (body.length) {
      case 1: {
        const func = body[0];
        if (
          func?.type === "ExpressionStatement" &&
          func.expression.type === "CallExpression" &&
          func.expression.callee.type === "MemberExpression" &&
          func.expression.callee.object.type === "FunctionExpression"
        ) {
          return func.expression.callee.object.body;
        }
        break;
      }
      case 2: {
        const func = body[1];
        if (
          func?.type === "ExpressionStatement" &&
          func.expression.type === "CallExpression" &&
          func.expression.callee.type === "FunctionExpression"
        ) {
          const block = func.expression.callee.body;
          // Skip `var window = this;`
          block.body.splice(0, 1);
          return block;
        }
        break;
      }
    }
    throw "unexpected structure";
  })();
  // Filter relevant statements
  block.body = block.body.filter((node: ESTree.Statement) => {
    if (node.type === "ExpressionStatement") {
      if (node.expression.type === "AssignmentExpression") {
        return true;
      }
      return node.expression.type === "Literal";
    }
    return true;
  });
  return block.body;
}
3

Extract Solvers

Scan statements to identify signature and n-parameter solver functions
export function getSolutions(
  statements: ESTree.Statement[],
): Record<string, ESTree.ArrowFunctionExpression[]> {
  const found = {
    n: [] as ESTree.ArrowFunctionExpression[],
    sig: [] as ESTree.ArrowFunctionExpression[],
  };
  for (const statement of statements) {
    const n = extractN(statement);
    if (n) {
      found.n.push(n);
    }
    const sig = extractSig(statement);
    if (sig) {
      found.sig.push(sig);
    }
  }
  return found;
}
4

Setup Environment

Inject runtime setup code to emulate browser globals
src/yt/solver/setup.ts
export const setupNodes = parse(`
if (typeof globalThis.XMLHttpRequest === "undefined") {
    globalThis.XMLHttpRequest = { prototype: {} };
}
const window = Object.create(null);
if (typeof URL === "undefined") {
    window.location = {
        hash: "",
        host: "www.youtube.com",
        hostname: "www.youtube.com",
        href: "https://www.youtube.com/watch?v=yt-dlp-wins",
        origin: "https://www.youtube.com",
        // ... more properties
    };
} else {
    window.location = new URL("https://www.youtube.com/watch?v=yt-dlp-wins");
}
if (typeof globalThis.document === "undefined") {
    globalThis.document = Object.create(null);
}
if (typeof globalThis.navigator === "undefined") {
    globalThis.navigator = Object.create(null);
}
if (typeof globalThis.self === "undefined") {
    globalThis.self = globalThis;
}
`).body;
5

Generate Executable Code

Convert the modified AST back to JavaScript using astring
program.body.splice(0, 0, ...setupNodes);
return generate(program);

Multi-Try Strategy

The preprocessor implements a resilient multi-try strategy when multiple potential solver implementations are found:
function multiTry(
  generators: ESTree.ArrowFunctionExpression[],
): ESTree.ArrowFunctionExpression {
  return {
    type: "ArrowFunctionExpression",
    params: [
      {
        type: "Identifier",
        name: "_input",
      },
    ],
    body: {
      type: "BlockStatement",
      body: [
        // Try each generator and collect results in a Set
        // If exactly one unique result, return it
        // Otherwise, throw an error
      ],
    },
    // ...
  };
}
The multi-try strategy attempts all extracted solvers and validates that they produce a single consistent result, providing robustness against YouTube player variations.

Data Flow

1

Input Processing

Receive player JavaScript or preprocessed code with challenge requests
2

Preprocessing (if needed)

Parse, transform, and extract solver functions from player code
3

Solver Extraction

Execute preprocessed code to obtain callable solver functions
export function getFromPrepared(code: string): {
  n: ((val: string) => string) | null;
  sig: ((val: string) => string) | null;
} {
  const resultObj = { n: null, sig: null };
  Function("_result", code)(resultObj);
  return resultObj;
}
4

Challenge Solving

Apply solver functions to each challenge string
5

Response Generation

Return mapped results or errors for each request

Integration with yt-dlp

The library integrates with yt-dlp through Python bindings:
hatch_build.py
class CustomBuildHook(BuildHookInterface):
    def initialize(self, version, build_data):
        name, cmds, env = build_bundle_cmds()
        if cmds is None:
            raise RuntimeError(
                "One of 'pnpm', 'deno', 'bun', or 'npm' could not be found. "
                "Please install one of them to proceed with the build."
            )
        print(f"Building with {name}...")

        for cmd in cmds:
            subprocess.run(cmd, env=env, check=True)

        build_data["force_include"].update(
            {
                "dist/yt.solver.core.min.js": "yt_dlp_ejs/yt/solver/core.min.js",
                "dist/yt.solver.lib.min.js": "yt_dlp_ejs/yt/solver/lib.min.js",
            }
        )
The build process automatically detects and uses the first available JavaScript runtime (pnpm, Deno, Bun, or npm). All runtimes must produce identical output.

Bundling Architecture

The library produces multiple build outputs optimized for different use cases:

Build Outputs

OutputFormatPurposeDependencies
yt.solver.core.jsIIFEUnminified core solverExternal (meriyah, astring)
yt.solver.core.min.jsIIFEMinified core solverExternal (meriyah, astring)
yt.solver.lib.jsIIFEUnminified library bundleBundled
yt.solver.lib.min.jsIIFEMinified library bundleBundled
yt.solver.deno.lib.jsES ModuleDeno with npm importsnpm: specifiers
yt.solver.bun.lib.jsES ModuleBun with versioned importsVersioned externals

Rollup Configuration

The build uses Rollup with specialized plugins:
rollup.config.js
export default defineConfig([
  {
    input: "src/yt/solver/main.ts",
    output: {
      name: "jsc",
      globals: {
        astring: "astring",
        input: "input",
        meriyah: "meriyah",
      },
      file: "dist/yt.solver.core.min.js",
      compact: true,
      format: "iife",
      minifyInternalExports: true,
    },
    external: ["astring", "meriyah"],
    plugins: [
      nodeResolve(),
      sucrase({
        exclude: ["node_modules/**"],
        transforms: ["typescript"],
      }),
      license({
        banner: {
          commentStyle: "ignored",
          content: LICENSE_BANNER,
        },
      }),
      terser(),
      printHash(),
    ],
  },
  // Additional configurations...
]);

Dependencies

The library has minimal runtime dependencies:

meriyah

Fast, spec-compliant JavaScript parser (ISC License)

astring

Tiny JavaScript code generator from ESTree AST (MIT License)
package.json
{
  "dependencies": {
    "astring": "1.9.0",
    "meriyah": "6.1.4"
  }
}
The library is licensed under the Unlicense, but prebuilt wheels contain meriyah (ISC) and astring (MIT) bundled within them.

Build docs developers (and LLMs) love