Skip to main content

Overview

yt-dlp-ejs implements two distinct types of solvers to handle YouTube’s anti-bot protection mechanisms:

Signature (sig)

Decodes scrambled video URL signatures

N-Parameter (n)

Solves throttling parameter challenges
Both solvers are automatically extracted from YouTube’s player JavaScript and executed to solve challenges dynamically.

Signature (sig) Solving

What is Signature Solving?

YouTube scrambles video URL signatures to prevent unauthorized access. The signature solver extracts the descrambling function from the player code and applies it to scrambled signatures.

Why is it Needed?

Without solving the signature:
  • Video URLs remain inaccessible
  • Direct downloads fail with 403 errors
  • Player authentication cannot be bypassed

Implementation Details

The signature extractor searches for specific AST patterns in the player code:
src/yt/solver/sig.ts
const nsig: DeepPartial<ESTree.CallExpression> = {
  type: "CallExpression",
  callee: {
    or: [{ type: "Identifier" }, { type: "SequenceExpression" }],
  },
  arguments: [
    {},
    {
      type: "CallExpression",
      callee: {
        type: "Identifier",
        name: "decodeURIComponent",
      },
      arguments: [{}],
    },
  ],
};

Extraction Strategy

The signature extractor uses multiple patterns to handle player variations:
Matches logical expressions with sequence operations:
const logicalExpression: DeepPartial<ESTree.ExpressionStatement> = {
  type: "ExpressionStatement",
  expression: {
    type: "LogicalExpression",
    left: {
      type: "Identifier",
    },
    right: {
      type: "SequenceExpression",
      expressions: [
        {
          type: "AssignmentExpression",
          left: {
            type: "Identifier",
          },
          operator: "=",
          right: {
            type: "CallExpression",
            callee: {
              type: "Identifier",
            },
            arguments: {
              or: [
                [
                  {
                    type: "CallExpression",
                    callee: {
                      type: "Identifier",
                      name: "decodeURIComponent",
                    },
                    arguments: [{ type: "Identifier" }],
                    optional: false,
                  },
                ],
                // More argument patterns...
              ],
            },
            optional: false,
          },
        },
        // More expressions...
      ],
    },
    operator: "&&",
  },
};

Solver Generation

Once a signature call is identified, the extractor generates an arrow function:
src/yt/solver/sig.ts:259-286
export function extract(
  node: ESTree.Node,
): ESTree.ArrowFunctionExpression | null {
  // ... pattern matching logic ...

  if (!call) {
    continue;
  }

  // TODO: verify identifiers here
  return {
    type: "ArrowFunctionExpression",
    params: [
      {
        type: "Identifier",
        name: "sig",
      },
    ],
    body: {
      type: "CallExpression",
      callee: call.callee,
      arguments: call.arguments.map((arg): ESTree.Expression => {
        if (
          arg.type === "CallExpression" &&
          arg.callee.type === "Identifier" &&
          arg.callee.name === "decodeURIComponent"
        ) {
          return { type: "Identifier", name: "sig" };
        }
        return arg as unknown as ESTree.Expression;
      }),
      optional: false,
    },
    async: false,
    expression: false,
    generator: false,
  };
}
The extractor replaces decodeURIComponent(...) calls with the input parameter sig, creating a reusable solver function.

Usage Example

When a signature challenge is received:
const input = {
  type: "player",
  player: playerJavaScript,
  requests: [
    {
      type: "sig",
      challenges: [
        "scrambled_signature_1",
        "scrambled_signature_2"
      ]
    }
  ],
  output_preprocessed: false
};

const output = main(input);
// output.responses[0].data = {
//   "scrambled_signature_1": "decoded_signature_1",
//   "scrambled_signature_2": "decoded_signature_2"
// }

N-Parameter (n) Solving

What is N-Parameter Solving?

The n-parameter is YouTube’s throttling mechanism. YouTube uses it to limit playback speed for automated clients. The n-parameter solver extracts the computation function and solves challenges.

Why is it Needed?

Without solving the n-parameter:
  • Video playback is severely throttled
  • Download speeds are artificially limited
  • Streaming quality degrades significantly

Implementation Details

The n-parameter extractor looks for array-based solver patterns:
src/yt/solver/n.ts
const identifier: DeepPartial<ESTree.Node> = {
  or: [
    {
      type: "VariableDeclaration",
      kind: "var",
      declarations: {
        anykey: [
          {
            type: "VariableDeclarator",
            id: {
              type: "Identifier",
            },
            init: {
              type: "ArrayExpression",
              elements: [
                {
                  type: "Identifier",
                },
              ],
            },
          },
        ],
      },
    },
    {
      type: "ExpressionStatement",
      expression: {
        type: "AssignmentExpression",
        left: {
          type: "Identifier",
        },
        operator: "=",
        right: {
          type: "ArrayExpression",
          elements: [
            {
              type: "Identifier",
            },
          ],
        },
      },
    },
  ],
} as const;

Extraction Strategy

The n-parameter extractor uses two complementary approaches:
1

Array Pattern Matching

Search for variable declarations or assignments with single-element arrays:
src/yt/solver/n.ts:119-148
if (node.type === "VariableDeclaration") {
  for (const declaration of node.declarations) {
    if (
      declaration.type !== "VariableDeclarator" ||
      !declaration.init ||
      declaration.init.type !== "ArrayExpression" ||
      declaration.init.elements.length !== 1
    ) {
      continue;
    }
    const [firstElement] = declaration.init.elements;
    if (firstElement && firstElement.type === "Identifier") {
      return makeSolverFuncFromName(firstElement.name);
    }
  }
} else if (node.type === "ExpressionStatement") {
  const expr = node.expression;
  if (
    expr.type === "AssignmentExpression" &&
    expr.left.type === "Identifier" &&
    expr.operator === "=" &&
    expr.right.type === "ArrayExpression" &&
    expr.right.elements.length === 1
  ) {
    const [firstElement] = expr.right.elements;
    if (firstElement && firstElement.type === "Identifier") {
      return makeSolverFuncFromName(firstElement.name);
    }
  }
}
2

Fallback Try-Catch Pattern

Search for functions with try-catch blocks containing specific return patterns:
src/yt/solver/n.ts:74-116
const catchBlockBody = [
  {
    type: "ReturnStatement",
    argument: {
      type: "BinaryExpression",
      left: {
        type: "MemberExpression",
        object: {
          type: "Identifier",
        },
        computed: true,
        property: {
          type: "Literal",
        },
        optional: false,
      },
      right: {
        type: "Identifier",
      },
      operator: "+",
    },
  },
] as const;

// Later in extraction:
const tryNode = block.body.at(-2);
if (
  tryNode?.type !== "TryStatement" ||
  tryNode.handler?.type !== "CatchClause"
) {
  return null;
}
const catchBody = tryNode.handler!.body.body;
if (matchesStructure(catchBody, catchBlockBody)) {
  return makeSolverFuncFromName(name);
}
3

Generate Solver Function

Create an arrow function wrapper around the identified solver:
src/yt/solver/n.ts:152-179
function makeSolverFuncFromName(name: string): ESTree.ArrowFunctionExpression {
  return {
    type: "ArrowFunctionExpression",
    params: [
      {
        type: "Identifier",
        name: "n",
      },
    ],
    body: {
      type: "CallExpression",
      callee: {
        type: "Identifier",
        name: name,
      },
      arguments: [
        {
          type: "Identifier",
          name: "n",
        },
      ],
      optional: false,
    },
    async: false,
    expression: false,
    generator: false,
  };
}

Usage Example

When an n-parameter challenge is received:
const input = {
  type: "preprocessed",
  preprocessed_player: cachedPreprocessedPlayer,
  requests: [
    {
      type: "n",
      challenges: [
        "throttle_param_1",
        "throttle_param_2"
      ]
    }
  ]
};

const output = main(input);
// output.responses[0].data = {
//   "throttle_param_1": "solved_param_1",
//   "throttle_param_2": "solved_param_2"
// }

Solver Extraction Flow

Multi-Solver Resilience

Both extractors may find multiple potential implementations in the player code. The preprocessor handles this through the multi-try strategy:
src/yt/solver/solvers.ts:87-104
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;
}
Each solver type can have multiple candidates. The multi-try wrapper executes all candidates and validates consistency.

Pattern Matching Utilities

Both extractors use a sophisticated pattern matching system:
import { matchesStructure } from "../../utils.ts";
import { type DeepPartial } from "../../types.ts";
The matchesStructure function allows for:
  • Partial matching: Only specified properties need to match
  • Alternative patterns: or arrays for multiple valid structures
  • Array matching: anykey for flexible array element checks
if (matchesStructure(node, identifier)) {
  // Process matching node
}

Error Handling

Solvers gracefully handle failures:
src/yt/solver/main.ts:11-40
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}`,
    };
  }
});
If a solver cannot be extracted or fails during execution, detailed error messages including stack traces are returned to aid debugging.

Performance Considerations

Preprocessing Cache

Preprocessed player code can be cached and reused across requests, avoiding repeated AST parsing

Batch Solving

Multiple challenges can be solved in a single request, reducing overhead
// Efficient: Preprocess once, solve many
const preprocessed = preprocessPlayer(playerJS);

const result1 = main({
  type: "preprocessed",
  preprocessed_player: preprocessed,
  requests: [{ type: "sig", challenges: batch1 }]
});

const result2 = main({
  type: "preprocessed",
  preprocessed_player: preprocessed,
  requests: [{ type: "n", challenges: batch2 }]
});

Build docs developers (and LLMs) love