Inside yt-dlp: Architecture and Runtime Logic
A runtime-grounded walkthrough of how yt-dlp actually works today, from option parsing and YoutubeDL orchestration to YouTube token flows, networking handlers, downloader selection, postprocessing, plugins, and packaging.
TL;DR
- `yt-dlp` is not just `URL -> extractor -> downloader`; the real runtime also includes config merging, option normalization, plugin loading, updater logic, request-handler selection, format sorting, postprocessing, cache/archive handling, and Python embedding support.
- `YoutubeDL` is the central orchestrator that normalizes options, coordinates extraction, format selection, downloader choice, output policy, hooks, and postprocessors.
- Modern YouTube support is a multi-part pipeline involving Innertube clients, player JS caching, JSC challenge solving, `yt-dlp-ejs`, JS runtimes, and PO Tokens.
- The networking stack is abstracted through `RequestDirector` and `RequestHandler`, not a single hardwired HTTP backend.
- Build tooling, plugins, updater behavior, devscripts, and distribution form are part of the real architecture, not just repo-side extras.
This note reflects the yt-dlp codebase as of April 2, 2026.
Overview
This post is an SSOT-style overview of the actual structure and runtime flow of the yt-dlp repository as of the current checkout.
It goes beyond the old simplified model of URL -> extractor -> downloader and also covers the subsystems that matter in the current codebase, including YouTube EJS/JSC/PO Token handling, the request handler layer, impersonation, plugins, updating, build/distribution, and Python embedding.
The goals of this document are:
- To help readers quickly understand the real layered structure of
yt-dlp - To keep the explanation traceable to the current implementation through core symbols and files
Notes:
- This repository is not a GUI app. It is the main Python-based CLI/library codebase.
- This document prioritizes the current implementation. README wording may be more user-friendly, but this write-up focuses on runtime architecture.
- To avoid churn, it uses files and symbols rather than fragile details like exact line numbers.
1. High-Level Structure
yt-dlp can be divided into the following major layers:
CLI / Python API
-> option/config parsing
-> YoutubeDL orchestrator
-> extractor selection and metadata extraction
-> format sorting/selection
-> downloader selection and actual download
-> postprocessor pipeline
-> file/metadata/archive/cache updates
Supporting subsystems
-> Networking RequestDirector + RequestHandler
-> Cookies / Proxy / SSL / Impersonation
-> Plugin loading
-> Updater
-> YouTube-specific EJS/JSC/PO Token handling
-> Build / Packaging / Embedding support
Key files are broadly organized as follows:
| Responsibility | Main files |
|---|---|
| CLI entry point | yt_dlp/__main__.py, yt_dlp/__init__.py |
| Option/config parsing | yt_dlp/options.py |
| Overall orchestration | yt_dlp/YoutubeDL.py |
| Site-specific extractors | yt_dlp/extractor/ |
| Protocol-specific downloaders | yt_dlp/downloader/ |
| Postprocessors | yt_dlp/postprocessor/ |
| Networking layer | yt_dlp/networking/ |
| Plugin system | yt_dlp/plugins.py |
| Update logic | yt_dlp/update.py |
| Cache | yt_dlp/cache.py |
| Build/packaging | pyproject.toml, Makefile, bundle/, devscripts/ |
2. Actual Entry Point and Execution Lifecycle
2.1 CLI entry point
When run from the CLI, the outermost entry point is yt_dlp/__main__.py.
# python -m yt_dlp
import yt_dlp
if __name__ == '__main__':
yt_dlp.main()
The real main loop lives in yt_dlp/__init__.py, in main() and _real_main().
A simplified runtime flow looks like this:
__main__.py
-> yt_dlp.main(argv)
-> _real_main(argv)
-> parse_options(argv)
-> parseOpts()
-> get_urls()
-> set_compat_opts()
-> validate_options()
-> get_postprocessors()
-> build ydl_opts
-> pre-process stage
-> plugin loading
-> cache removal option handling
-> updater handling
-> list-impersonate-targets and other special commands
-> with YoutubeDL(ydl_opts) as ydl:
-> download(all_urls) or download_with_info_file(...)
So although documentation often makes it look like main() -> parseOpts() -> YoutubeDL.download(), the real path also includes config loading, option validation, updater logic, plugin setup, postprocessor construction, JS runtime setup, and remote component policy.
2.2 Config file loading
parseOpts() does not only read command-line arguments. It merges configuration files using the following priority flow:
- Main config file specified with
--config-locations - Portable config
- Home config
- User config
- System config
Key points:
- Config files use the same syntax as CLI options.
- If
--ignore-configis encountered, later config loading may stop. - Candidates include the current working directory, executable location, user config directories, and system config directories.
So yt-dlp is not just a tool that reads command-line flags once. It builds a final option set by composing a config hierarchy and then executes from that merged result.
2.3 Option validation and normalization
parse_options() in yt_dlp/__init__.py takes the result of parseOpts() and performs tasks such as:
- applying compat options
- validating numbers, regexes, and format expressions
- normalizing retry policies
- validating output templates
- parsing
cookies-from-browser - parsing impersonation targets
- normalizing metadata parser actions
- processing playlist items, geo bypass, headers, and external downloader settings
- building the postprocessor list
- adjusting quiet/simulate/list-only behavior
- injecting
js_runtimesandremote_componentsintoydl_opts
So ydl_opts is not a raw copy of user input. It is an execution-ready option dictionary that has already been normalized and dependency-resolved.
2.4 What happens in pre-process
Before actual downloading starts, _real_main() may also handle:
- adding extra plugin directories and loading plugins
- cache removal (
--rm-cache-dir) - self-update (
-U,--update-to) - printing impersonation target lists
- handling
--load-info-json
That means “give it a URL and it goes straight to the extractor” is now an oversimplification.
3. What the YoutubeDL object does
3.1 YoutubeDL is not just a downloader
YoutubeDL in yt_dlp/YoutubeDL.py is effectively the central runtime object.
Major state includes:
_ies: registered extractor instances_pps: postprocessors grouped by execution timingparams: normalized runtime optionscache: global cache object_request_director: network request director- hooks, progress state, playlist state, output stream settings
During initialization, YoutubeDL also handles:
- plugin-loading related setup
- color policy
- outdated version warnings
- JS runtime normalization
- remote component allow-list normalization
- default HTTP header and Cookie header adjustments
- validation of global impersonation target support
So this is not “the class that downloads files.” It is the runtime hub that ties together extraction, networking, format selection, postprocessing, output policy, cache, and update-related behavior.
3.2 The Python API uses the same core object
Both the CLI and Python embedding use the same YoutubeDL object.
Representative embedding APIs include:
YoutubeDL.download(urls)YoutubeDL.extract_info(url, download=False/True)YoutubeDL.download_with_info_file(path)YoutubeDL.add_post_processor(...)YoutubeDL.sanitize_info(info)
So the CLI and Python API are not separate implementations. They share the same runtime core.
4. The Extractor Pipeline
4.1 Extractor registration and ordering
Extractors live under yt_dlp/extractor/.
yt_dlp/extractor/__init__.py registers the extractor plugin spec and builds the extractor class list.
Important points:
- extractor order matters
- the first matching extractor handles the URL
GenericIEusually acts as the final fallback- extractor plugins can take priority over built-in extractors
As of the current checkout, the number of extractor classes is very large. In local snapshots it is on the order of thousands. The exact number varies by release, so it is better to treat that as scale rather than a stable fact.
4.2 What extract_info() really does
YoutubeDL.extract_info() does more than match _VALID_URL and choose an extractor.
In practice, it does the following:
- optionally restricts to a specific
ie_key - iterates over registered extractors
- checks
ie.suitable(url) - may print broken-site warnings
- may use an extractor-generated
temp_idto pre-check the archive - calls
__extract_info()when a match is found
So even before extractor matching fully completes, archive integration and error policy can already affect the flow.
4.3 What __extract_info() does
__extract_info() calls ie.extract(url) and then handles:
- applying header cookies
- wrapping extractor exceptions and handling retry/re-extraction cases
- common handling for
UserNotLive,GeoRestrictedError,ExtractorError, and others - wrapping old list-based results into compatibility form
- injecting default extra info
- passing the result to
process_ie_result()ifprocess=True
The key point is that the extractor output is not necessarily the final info object.
4.4 Branching in process_ie_result()
In the current implementation, process_ie_result() routes results very differently depending on type.
Common _type values include:
videourlurl_transparentplaylistmulti_videocompat_list
Meaning:
video: goes directly toprocess_video_result()url: sends another URL back through the extractor pipelineurl_transparent: preserves outer-page metadata while extracting and processing an inner URLplaylist/multi_video: branches into playlist-specific handlingcompat_list: recursively reprocesses old compatibility-style results
In addition, additional_urls may trigger further recursive extraction during processing of the same video.
So process_ie_result() is not just a “playlist handler.” It is the main recursive normalization hub for extractor results.
4.5 Playlist handling
Playlist-like results are handled in __process_playlist().
It is responsible for:
- building shared playlist info
- calculating the requested entry set
- respecting lazy playlist behavior
- deciding whether playlist metadata files should be written
- applying reverse/random/lazy policies
- recursively processing each entry
- integrating concat-playlist postprocessing when needed
So a playlist is not simply an entries loop. It is a distinct execution mode with lazy evaluation and output-policy behavior.
5. process_video_result() and format selection
5.1 What actually happens at this stage
process_video_result() is one of the places where the current yt-dlp implementation performs the most correction and normalization work.
This stage includes:
- validating that
idexists - normalizing string and numeric fields
- fixing chapter structures
- normalizing thumbnails
- filling common fields
- normalizing subtitles and automatic captions
- computing
requested_subtitles - collecting the
formatslist - detecting and filtering DRM
- handling live / post-live / live-from-start cases
- dropping malformed formats
- filling format sort fields
- uniquifying format IDs
- checking format availability when needed
- running pre-process hooks
- applying match filters
- doing post-extract handling
- handling list-only / list-formats / interactive format selection
- computing the final requested format/range combinations
So even if an extractor already returns formats, the download is not yet ready. YoutubeDL still performs substantial normalization right before download selection.
5.2 Format sorting
Format sorting is handled by FormatSorter.
It is more than just quality, tbr, and height. It also accounts for:
- whether video/audio streams exist
- codec preference
- resolution and fps
- language
- source preference
- protocol
- HDR / channel count / dynamic range
- extractor-provided
_format_sort_fields
Sites like YouTube may inject their own _format_sort_fields.
5.3 Format selector syntax
The user format expression is parsed by build_format_selector().
Core operators include:
/: fallback+: merge,: multiple selector groups[]: filters(): grouping
This is not a simple string matcher. It is a dedicated mini-language parser.
Example:
bestvideo[height<=1080]+bestaudio/best
Meaning:
- first try the best video at 1080p or below
- merge it with the best audio
- if that fails, fall back to
best
5.4 Default format selection also depends on ffmpeg availability
yt-dlp may change its default format-selection policy when ffmpeg is unavailable.
So it is not always accurate to summarize it as simply bestvideo+bestaudio/best.
The current default behavior depends on factors such as:
- whether ffmpeg is available
- compat options
- whether multiple audio/video streams are allowed
- the format structure provided by the extractor
5.5 Download section selection is combined here too
Features like --download-sections are not just downloader-level options.
process_video_result() builds requested_ranges, then takes the Cartesian product with selected formats to create concrete download jobs.
So section-based downloading is combined after format selection.
6. The current structure of YouTube-specific handling
Older mental models of yt-dlp often reduce YouTube support to “download the player JS, find the obfuscated signature function, and use JSInterpreter to decode it.”
That is still part of the story, but it is no longer enough to describe current YouTube handling.
6.1 Core elements of current YouTube handling
Current YouTube video extraction usually involves the following components together:
- acquiring webpage / ytcfg / player response data
- making Innertube API calls
- combining multiple player clients
- handling visitor data / data sync ID / auth state
- downloading and caching player JS
- solving JS challenges through JSC director/provider logic
- integrating
yt-dlp-ejsand external JS runtimes - fetching/caching PO Tokens
- handling subtitles PO Tokens
- managing live, post-live, manifestless, premium, and incomplete formats
So current YouTube support is much more than “HTML parsing + signature decrypt.”
6.2 Player clients and Innertube
The YouTube extractor can operate with different player client contexts. For example, web, iOS, Android, and mweb clients may expose different responses, formats, or restrictions.
Important runtime fields include:
visitor_datadata_sync_idinnertube_hostinnertube_keyplayer_client
So current YouTube support is closer to client-aware API orchestration than to simple webpage scraping.
6.3 Player JS and caching
The YouTube extractor downloads player JS and uses both memory and disk caches.
Relevant concepts:
_code_cache: in-memory cache of player JS code_player_cache: cache of player-derived dataCache: disk cache root, typically~/.cache/yt-dlp
Important point:
- the cache is not just a simple storage area for signature/nsig results
- keys are built around player URLs and player variants
- the same global cache infrastructure is used by non-YouTube extractors too
6.4 JSC: JavaScript challenge handling
During _real_initialize(), the YouTube extractor initializes two main directors:
- the PO Token director
- the JSC director
JSC is the JavaScript challenge-solving layer. It works through a provider-based model, where either built-in or external providers may solve the challenges.
This means:
- you should not assume
JSInterpreteralone solves every challenge - JS challenges may be solved in bulk through the provider/director structure
- runtime availability is tied to JS runtimes such as Node, Deno, Bun, or QuickJS
6.5 yt-dlp-ejs and JS runtimes
According to the current README and pyproject.toml, yt-dlp-ejs is a meaningful part of YouTube support.
Related elements include:
yt-dlp-ejsas a default optional dependency- JS runtimes such as Deno, Node, Bun, and QuickJS
- the
js_runtimesoption - the
remote_componentsoption
So current yt-dlp is no longer a pure “Python-only YouTube decoder” model. It can integrate with an external JS ecosystem when needed.
6.6 PO Tokens
PO Tokens are now a mandatory part of any serious explanation of YouTube support.
The current code explicitly includes concepts such as:
- GVS PO Token
- PO Token for player requests
- PO Token for subtitles
- provider/director-based fetching
- cache integration
- manual injection through extractor arguments
Relevant extractor args include:
po_tokenfetch_potpot_trace
In practical terms:
- some formats cannot be fully resolved without a PO Token
- subtitles may also require a PO Token
- formats missing PO Tokens are explicitly distinguished in docs and options
6.7 The current role of JSInterpreter
JSInterpreter in yt_dlp/jsinterp.py is still an important low-level component.
But it is no longer accurate to present it as the one central innovation that explains YouTube support by itself.
A better description today is:
JSInterpreterremains an important building block for some script and challenge handling- but the higher-level YouTube pipeline now also includes EJS, JSC, PO Tokens, Innertube orchestration, and client-aware behavior
So to understand modern YouTube support in yt-dlp, you should focus on the combined pipeline rather than on JSInterpreter alone.
7. Downloader selection and actual download
7.1 Protocol-based downloader selection
Downloader selection is handled by get_suitable_downloader() in yt_dlp/downloader/__init__.py.
Important points:
- selection depends on both protocol strings and option combinations
- a single stream URL does not always map to a single fixed downloader path
- external downloaders, ffmpeg, and native downloaders may all be involved depending on the case
Representative mappings are roughly:
| Protocol | Main path |
|---|---|
https / generic HTTP | HttpFD |
m3u8_native | usually HlsFD |
m3u8 | usually FFmpegFD |
http_dash_segments | DashSegmentsFD |
f4m | F4mFD |
rtmp family | RtmpFD or FFmpegFD |
mms, rtsp | RtspFD |
websocket_frag | WebSocketFragmentFD |
So saying only “m3u8 uses HlsFD” would be inaccurate for the current codebase.
7.2 HttpFD
HttpFD is the default path for ordinary HTTP downloads.
Key functionality includes:
- resume / Range requests
- chunked download
- progress hooks
- min/max filesize checks
- retry manager integration
- use of
info_dict['http_headers']and request extensions - passing impersonation extensions when needed
So it is much more than a simple response.read() loop.
7.3 HlsFD
HlsFD is the native HLS downloader.
Its responsibilities include:
- downloading and parsing m3u8 manifests
- checking whether the manifest is supported
- checking DRM and unsupported features
- handling AES-128 keys
- building fragment lists
- optionally delegating fragment downloads to external downloaders
- falling back to ffmpeg for live HLS or GenericIE-related cases
So even HLS handling is much more complex than just “parse the manifest and download TS segments in order.”
7.4 ffmpeg delegation
Current yt-dlp makes active use of FFmpegFD in many cases.
Representative examples include:
- the default path for many
m3u8cases - live HLS
- manifests that native HLS cannot handle
- section downloads
- cases that require merged stdout output
So ffmpeg is not only a postprocessing tool. It is also deeply involved in the downloader layer.
7.5 External downloaders and impersonation constraints
External downloaders are powerful, but they are not always used unconditionally. For example, if a request requires impersonation, external downloader selection may be restricted.
So downloader selection is not just “the user picked aria2c.” It also depends on request capabilities and extractor-imposed requirements.
8. The networking request layer
8.1 Current structure
The core networking architecture is based on RequestDirector + RequestHandler.
Flow:
YoutubeDL.urlopen()
-> RequestDirector.send(Request)
-> compute preferences among available handlers
-> choose handlers that can validate the request
-> send via the selected handler
-> return Response
So “it directly calls urllib” is no longer an accurate model.
8.2 RequestDirector
RequestDirector is responsible for:
- storing registered handlers
- computing handler preference per request
- calling
validate()on each handler - skipping unsupported or failing handlers and trying the next one
- returning a final
Responseor raisingNoSupportingHandlers
So networking is not hardwired to a single backend. It goes through per-request capability matching.
8.3 RequestHandler
RequestHandler is an abstraction layer.
Shared concerns include:
- supported URL scheme checks
- proxy scheme checks
- extension checks
- header merging
- cookiejar selection
- timeout calculation
- SSL context creation
Request extensions include fields such as:
cookiejartimeoutlegacy_sslkeep_header_casingimpersonate
So a request carries more than just a URL. It also carries capability requirements and execution metadata.
8.4 Main handlers
The main handlers worth remembering are:
UrllibRH: the default HTTP handlerCurlCFFIRH:curl_cffi-based handlerWebsocketRH: websocket support- impersonation-capable handler layers
Important point:
- the actual handler selected depends on the request and installed dependencies
curl_cffiis not only a performance option, but also important for browser impersonation
8.5 Impersonation
Impersonation is now an important part of yt-dlp networking.
Relevant pieces include:
--impersonate--list-impersonate-targets- extractor-level
impersonate=Trueor target selection - request extensions that carry impersonation targets
- support through
curl_cffi
So the current networking model goes beyond cookies, proxies, and TLS. It includes browser-like request emulation as a real runtime capability.
9. Postprocessing and file output
9.1 Postprocessors are assembled from options
The postprocessor pipeline is built by get_postprocessors().
So when the user passes options such as --extract-audio, --embed-thumbnail, or --sponsorblock-remove, those are internally converted into postprocessor specs.
Representative postprocessors include:
MetadataParserSponsorBlockFFmpegSubtitlesConvertorFFmpegThumbnailsConvertorFFmpegExtractAudioFFmpegVideoRemuxerFFmpegVideoConvertorFFmpegEmbedSubtitleModifyChaptersFFmpegMetadataEmbedThumbnailFFmpegSplitChaptersXAttrMetadataFFmpegConcatExec
9.2 Execution timing (when) matters
Postprocessors do not run only after download. The current pipeline supports multiple execution stages.
Examples:
pre_processafter_filterbefore_dl- default post-process
playlist
So postprocessing is not “run ffmpeg once after download.” It can span pre-download, post-download, and playlist-level stages.
9.3 FFmpegMergerPP
The most common example is merging bestvideo + bestaudio.
The real merge logic considers:
requested_formats- audio/video mapping for each stream
- whether m3u8 AAC fixup is required
- temporary file creation followed by rename
So while a simple ffmpeg -i a -i b -c copy command illustrates the idea, the actual implementation handles stream mapping and fixups more carefully.
9.4 Final file output is shaped by both the downloader and postprocessors
Final output paths and side files are determined by multiple layers working together.
Relevant elements include:
outtmplpaths.partand other temporary files- info JSON / description / thumbnails / subtitles
- playlist-level metadata files
- download archive
- final extensions produced by postprocessors
So the output is not just “one downloaded file.” It is often a coordinated set of metadata files and derived artifacts.
10. Cache, archive, and filesystem state
10.1 Cache
Cache in yt_dlp/cache.py is the global cache implementation.
Default root:
~/.cache/yt-dlp
Or another location depending on XDG_CACHE_HOME or cachedir.
The cache stores more than just YouTube player data.
Examples:
- YouTube player-derived data
- PO Token provider results
- extractor-specific bearer tokens / device IDs / auth state
- site-specific authentication cache
So the cache is a general-purpose infrastructure component in current yt-dlp.
10.2 Download archive is different from cache
download_archive is used to prevent duplicate downloads, while the cache stores reusable computed state or helper data.
They serve different purposes.
- cache: performance/session/helper state
- archive: record of already processed videos
That is also why archive pre-checking can happen during extract_info().
11. Plugin system
Current yt-dlp officially supports extractor and postprocessor plugins.
11.1 Plugin forms
Plugin namespaces:
yt_dlp_plugins.extractoryt_dlp_plugins.postprocessor
Plugin types:
- extractor plugins
- postprocessor plugins
11.2 Load locations
Plugins can be discovered from multiple locations:
- user/system config directories
- adjacent
yt-dlp-pluginsdirectory near the executable PYTHONPATH- packaged
.zip,.egg,.whldistributions
So plugins are not just internal extension points. They are a real externally distributable extension system.
11.3 Priority and caveats
Important runtime properties:
- extractor plugins are matched automatically
- extractor plugins can take priority over built-in extractors
- all plugins are loaded without trust verification
YTDLP_NO_PLUGINScan disable all plugins globally
So the plugin layer deserves to be treated as a first-class architectural axis.
12. Updater and release channels
Current yt-dlp includes a built-in updater.
Core class:
Updaterinyt_dlp/update.py
Core concepts:
stablenightlymaster- and, when supported, other repository channels
The update flow is roughly:
- identify the current variant/platform
- resolve the requested channel/tag
- query the GitHub release API
- inspect assets and update specs
- choose a compatible build
- download it and handle restart/update application if possible
Important notes:
- the updater matters most for release-binary scenarios
- its behavior differs from source checkouts or pip installs
warn_when_outdatedis also tied into initialization-time behavior
So the updater is not just a README side feature. It is part of the actual runtime system.
13. Build, packaging, and distribution
This repository does not only contain Python package source. It also contains the machinery to build and package release artifacts.
13.1 pyproject.toml
pyproject.toml defines:
- project metadata
- Python version requirements
- optional dependency groups
- entry point (
yt-dlp = yt_dlp:main) - hatch build targets
- test / lint / dev dependency groups
The default optional dependencies currently include areas such as:
- enhanced networking packages
pycryptodomexwebsocketsrequestsyt-dlp-ejs
So it is no longer accurate to think of the project as “just Python standard library core.” The optional ecosystem is a meaningful part of the system.
13.2 Makefile
The Makefile ties together source distribution and generated artifacts.
Representative targets include:
lazy-extractorsyt-dlpyt-dlp-extraREADME.mdyt-dlp.1- completions
yt-dlp.tar.gztest,offlinetest,codetest
So the repository directly generates items such as:
- zipimport-based Unix executables
- manpages
- shell completions
- lazy extractor files
- parts of generated README/docs
13.3 Lazy extractors
devscripts/make_lazy_extractors.py generates yt_dlp/extractor/lazy_extractors.py to reduce startup cost.
Since the extractor count is very large, import-cost management is an important concern in build and distribution too.
13.4 Distribution forms
Based on the repository and README, representative distribution forms include:
- source tree
- pip package
- PyInstaller standalone executable
- zipimport-based Unix binary
- extra binary flows that include EJS components
So “source install” and “release binary” differ in meaningful internal ways, including updater behavior.
14. Devscripts and generator-driven maintenance
devscripts/ is not a minor side directory. It is a key toolbox for repository maintenance.
Representative roles include:
- generating README content
- generating supported-sites docs
- generating changelog / issue-template material
- generating shell completions
- generating lazy extractors
- running tests
- handling bundle requirement/update scripts
So many project artifacts are maintained through scripts rather than by hand.
That is one reason this document should be SSOT-style: because generated or semi-generated outputs such as README and completions exist, the core structural explanation should remain explicit and grounded in the implementation.
15. The core from the Python embedding perspective
yt-dlp is both a CLI program and an embedding-friendly library.
The main embedding surface, as reflected in the README, includes:
- using
YoutubeDL()as a context manager - calling
download() - calling
extract_info(download=False)to fetch metadata only - calling
sanitize_info()to obtain JSON-serializable output - injecting progress hooks or a custom logger
- adding custom postprocessors
So external programs reuse the same core pipeline rather than calling a separate simplified engine.
Important practical points:
- parsing ordinary stdout is not a stable API
- output control should use
-J,--print, progress templates, or hooks - the return value of
extract_info()should not be assumed to be directly JSON-serializable
16. Condensed restatement of the current structure
16.1 Most compressed runtime flow
CLI / Python API
-> merge config files + CLI options
-> validate/normalize options
-> prepare plugins / updater / JS runtime
-> initialize YoutubeDL
-> match extractor
-> normalize extractor result (process_ie_result)
-> normalize video result (process_video_result)
-> sort/select formats
-> choose downloader
-> perform actual download
-> run postprocessor pipeline
-> write metadata files / archive / cache / final outputs
16.2 YouTube-specific flow in compressed form
webpage / ytcfg / player response / Innertube client combination
-> player JS cache
-> solve JSC challenges
-> optional EJS + JS runtime integration
-> handle player/GVS/subtitles PO Tokens
-> normalize formats/live/subtitles
-> choose downloader / ffmpeg path
16.3 Common misunderstandings
Misunderstanding: yt-dlp takes a URL, one extractor creates metadata, and it immediately downloads.
Reality:
- config loading, validation, and pre-processing happen first
- extractor results are recursively normalized across multiple result types
YoutubeDLheavily normalizes video results again- YouTube uses a complex pipeline including EJS, JSC, PO Tokens, and client orchestration
- networking is abstracted through RequestDirector + handler layers
- ffmpeg is deeply involved not just in postprocessing, but also in downloader selection
- plugins, update logic, build logic, and distribution model are all part of the real system
17. Conclusion
The most accurate way to understand current yt-dlp is to view it as:
a multi-layer CLI/library runtime with thousands of site extractors, combined with a format selector, downloader suite, request-handler layer, ffmpeg-based postprocessing, a YouTube-specific EJS/JSC/PO Token pipeline, and plugin/update/build systems
The core skeleton is still:
URL -> Extractor -> Formats -> Downloader -> PostProcessor
But what really matters in the current implementation is everything wrapped around that skeleton:
Config + Validation + Plugins + RequestDirector + Impersonation
+ YouTube EJS/JSC/PO Token + Update + Build/Distribution + Embedding API
After reading this document, it is more accurate to see yt-dlp no longer as a “download script,” but as a fairly large runtime/platform.