Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658This file intends to give a big picture overview of how mpv is structured.player/*.c: Essentially makes up the player applications, including the main() function and the playback loop. Generally, it accesses all other subsystems, initializes them, and pushes data between them during playback. The structure is as follows (as of commit e13c05366557cb): * main(): * basic initializations (e.g. init_libav() and more) * pre-parse command line (verbosity level, config file locations) * load config files (mp_parse_cfgfiles()) * parse command line, add files from the command line to playlist (m_config_parse_mp_command_line()) * check help options etc. (call handle_help_options()), possibly exit * call mp_play_files() function that works down the playlist: * run idle loop (idle_loop()), until there are files in the playlist or an exit command was given (only if --idle it set) * actually load and play a file in play_current_file(): * run all the dozens of functions to load the file and initialize playback * run a small loop that does normal playback, until the file is done or a command terminates playback (on each iteration, run_playloop() is called, which is rather big and complicated - it decodes some audio and video on each frame, waits for input, etc.) * uninitialize playback * determine next entry on the playlist to play * loop, or exit if no next file or quit is requested (see enum stop_play_reason) * call mp_destroy() * run_playloop(): * calls fill_audio_out_buffers() This checks whether new audio needs to be decoded, and pushes it to the AO. * calls write_video() Decode new video, and push it to the VO. * determines whether playback of the current file has ended * determines when to start playback after seeks * and calls a whole lot of other stuff (Really, this function does everything.) Things worth saying about the playback core: - most state is in MPContext (core.h), which is not available to the subsystems (and should not be made available) - the currently played tracks are in mpctx->current_track, and decoder state in track.dec/d_sub - the other subsystems rarely call back into the frontend, and the frontend polls them instead (probably a good thing) - one exceptions are wakeup callbacks, which notify a "higher" component of a changed situation in a subsystem I like to call the player/*.c files the "frontend".ta.h & ta.c: Hierarchical memory manager inspired by talloc from Samba. It's like a malloc() with more features. Most importantly, each talloc allocation can have a parent, and if the parent is free'd, all children will be free'd as well. The parent is an arbitrary talloc allocation. It's either set by the allocation call by passing a talloc parent, usually as first argument to the allocation function. It can also be set or reset later by other calls (at least talloc_steal()). A talloc allocation that is used as parent is often called a talloc context. One very useful feature of talloc is fast tracking of memory leaks. ("Fast" as in it doesn't require valgrind.) You can enable it by setting the MPV_LEAK_REPORT environment variable to "1": export MPV_LEAK_REPORT=1 Or permanently by building with --enable-ta-leak-report. This will list all unfree'd allocations on exit. Documentation can be found here: http://git.samba.org/?p=samba.git;a=blob;f=lib/talloc/talloc.h;hb=HEAD For some reason, we're still using API-compatible wrappers instead of TA directly. The talloc wrapper has only a subset of the functionality, and in particular the wrappers abort() on memory allocation failure. Note: unlike tcmalloc, jemalloc, etc., talloc() is not actually a malloc replacement. It works on top of system malloc and provides additional features that are supposed to make memory management easier.player/command.c: This contains the implementation for client API commands and properties. Properties are essentially dynamic variables changed by certain commands. This is basically responsible for all user commands, like initiating seeking, switching tracks, etc. It calls into other player/*.c files, where most of the work is done, but also calls other parts of mpv.player/core.h: Data structures and function prototypes for most of player/*.c. They are usually not accessed by other parts of mpv for the sake of modularization.player/client.c: This implements the client API (libmpv/client.h). For the most part, this just calls into other parts of the player. This also manages a ringbuffer of events from player to clients.options/options.h, options/options.c options.h contains the global option struct MPOpts. The option declarations (option names, types, and MPOpts offsets for the option parser) are in options.c. Most default values for options and MPOpts are in mp_default_opts at the end of options.c. MPOpts is unfortunately quite monolithic, but is being incrementally broken up into sub-structs. Many components have their own sub-option structs separate from MPOpts. New options should be bound to the component that uses them. Add a new option table/struct if needed. The global MPOpts still contains the sub-structs as fields, which serves to link them to the option parser. For example, an entry like this may be typical: {"", OPT_SUBSTRUCT(demux_opts, demux_conf)}, This directs the option access code to include all options in demux_conf into the global option list, with no prefix (""), and as part of the MPOpts.demux_opts field. The MPOpts.demux_opts field is actually not accessed anywhere, and instead demux.c does this: struct m_config_cache *opts_cache = m_config_cache_alloc(demuxer, global, &demux_conf); struct demux_opts *opts = opts_cache->opts; ... to get a copy of its options. See m_config_core.h (below) how to access options. The actual option parser is spread over m_option.c, m_config_frontend.c, and parse_commandline.c, and uses the option table in options.c.options/m_config_*.h & m_config_*.c: Code for querying and managing options. m_config_frontend.h contains declarations for the "legacy-ish" global m_config struct, while m_config_core.h provides ways to access options in a threads-safe way anywhere, like m_config_cache_alloc(). m_config_cache_alloc() lets anyone read, observe, and write options in any thread. The only state it needs is struct mpv_global, which is an opaque type that can be passed "down" the component hierarchy. For safety reasons, you should not pass down any pointers to option structs (like MPOpts), but instead pass down mpv_global, and use m_config_cache_alloc() (or similar) to get a synchronized copy of the options.input/input.c: This translates keyboard input coming from VOs and other sources (such as remote control devices like Apple IR or client API commands) to the key bindings listed in the user's (or the builtin) input.conf and turns them into items of type struct mp_cmd. These commands are queued, and read by playloop.c. They get pushed with run_command() to command.c. Note that keyboard input and commands used by the client API are the same. The client API only uses the command parser though, and has its own queue of input commands somewhere else.common/msg.h: All terminal output must go through mp_msg().stream/*: File input is implemented here. stream.h/.c provides a simple stream based interface (like reading a number of bytes at a given offset). mpv can also play from http streams and such, which is implemented here. E.g. if mpv sees "http://something" on the command line, it will pick stream_lavf.c based on the prefix, and pass the rest of the filename to it. Some stream inputs are quite special: stream_dvdnav.c turns DVDs into mpeg streams (DVDs are actually a bunch of vob files etc. on a filesystem), Some stream inputs are just there to invoke special demuxers, like stream_mf.c. (Basically to make the prefix "mf://" do something special.)demux/: Demuxers split data streams into audio/video/sub streams, which in turn are split in packets. Packets (see packet.h) are mostly byte chunks tagged with a playback time (PTS). These packets are passed to the decoders. Most demuxers have been removed from this fork, and the only important and "actual" demuxers left are demux_mkv.c and demux_lavf.c (uses libavformat). There are some pseudo demuxers like demux_cue.c. The main interface is in demux.h. The stream headers are in stheader.h. There is a stream header for each audio/video/sub stream, and each of them holds codec information about the stream and other information. demux.c is a bit big, the main reason being that it contains the demuxer cache, which is implemented as a list of packets. The cache is complex because it support seeking, multiple ranges, prefetching, and so on.filters/: Filter related code. filter.c contains the generic filtering framework which converts input frames to output frames (audio, video, or demux packet data). f_decoder_wrapper.c is a source filter which connects the frontend with the actual audio and video decoders. f_output_chain.c handles VO/AO output conversions. f_autoconvert.c automatically inserts the appropriate conversion filters if format conversion is needed.video/: This contains several things related to audio/video decoding, as well as video filters. mp_image.h and img_format.h define how mpv stores decoded video frames internally.video/decode/: vd_*.c are video decoders. (There's only vd_lavc.c left.)video/filter/: vf_*.c are video filters. They are fed by the video decoder, and output the filtered images to the VOs. By default, no video filters are used.video/out/: Video output. They also create GUI windows and handle user input. In most cases, the windowing code is shared among VOs, like x11_common.c for X11 and w32_common.c for Windows. The VOs stand between frontend and windowing code. vo_gpu and vo_gpu_next can pick a windowing system at runtime, e.g. the same binary can provide both X11 and Cocoa support on macOS. VOs can be reconfigured at runtime. A vo_reconfig() call can change the video resolution and format, without destroying the window. vo_gpu should be taken as reference.audio/: format.h/format.c define the uncompressed audio formats. (As well as some compressed formats used for spdif.)audio/decode/: ad_*.c handle audio decoding. ad_lavc.c is the decoder using ffmpeg. ad_spdif.c is not really a decoder, but is used for compressed audio passthrough.audio/filter/: Audio filters. af_scaletempo2 is inserted by default if playback is different from normal speed.audio/out/: Audio outputs. Unlike VOs, AOs can't be reconfigured on a format change. On audio format changes, the AO will simply be closed and re-opened. buffer.c is the wrapper to support for two types of audio APIs: push and pull. ao.c calls into that. It contains generic code to deal with the data flow these APIs impose. Note that mpv synchronizes the video to the audio. That's the reason why buggy audio drivers can have a bad influence on playback quality.sub/: Contains subtitle and OSD rendering. osd.c/.h is actually the OSD code. It queries dec_sub.c to retrieve decoded/rendered subtitles. osd_libass.c is the actual implementation of the OSD text renderer (which uses libass, and takes care of all the tricky fontconfig/freetype API usage and text layouting). The VOs call osd.c to render OSD and subtitle (via e.g. osd_draw()). osd.c in turn asks dec_sub.c for subtitle overlay bitmaps, which relays the request to one of the sd_*.c subtitle decoders/renderers. Subtitle loading is in demux/. Normally, subtitles are loaded via demux_lavf.c. The subtitles are passed to dec_sub.c and the subtitle decoders in sd_*.c as they are demuxed. All text subtitles are rendered by sd_ass.c. If text subtitles are not in the ASS format, the libavcodec subtitle converters are used (lavc_conv.c). Text subtitles can be preloaded, in which case they are read fully as soon as the subtitle is selected. In this case, they are effectively stored in sd_ass.c's internal state.etc/: The files input.conf and builtin.conf are actually integrated into the mpv binary by the build system. They contain the default configs and keybindings.Best practices and Concepts within mpv======================================General contribution etc.-------------------------See: DOCS/contribute.mdError checking--------------If an error is relevant, it should be handled. If it's interesting, log theerror. However, mpv often keeps errors silent and reports failures somewhatcoarsely by propagating them upwards the caller chain. This is OK, as long asthe errors are not very interesting, or would require a developer to debug itanyway (in which case using a debugger would be more convenient, and thedeveloper would need to add temporary debug printfs to get extremely detailedinformation which would not be appropriate during normal operation).Basically, keep a balance on error reporting. But always check them, unless youhave a good argument not to.Memory allocation errors (OOM) are a special class of errors. Normally suchallocation failures are not handled "properly". Instead, abort() is called.(New code should use MP_HANDLE_OOM() for this.) This is done out of laziness andfor convenience, and due to the fact that MPlayer/mplayer2 never handled itcorrectly. (MPlayer varied between handling it correctly, trying to do so butfailing, and just not caring, while mplayer2 started using abort() for it.)This is justifiable in a number of ways. Error handling paths are notoriouslyuntested and buggy, so merely having them won't make your program more reliable.Having these error handling paths also complicates non-error code, due to theneed to roll back state at any point after a memory allocation.Take any larger body of code, that is supposed to handle OOM, and test whetherthe error paths actually work, for example by overriding malloc with a versionthat randomly fails. You will find bugs quickly, and often they will be veryannoying to fix (if you can even reproduce them).In addition, a clear indication that something went wrong may be missing. Onerror your program may exhibit "degraded" behavior by design. Consider a videoencoder dropping frames somewhere in the middle of a video due to temporaryallocation failures, instead of just exiting with an errors. In other cases, itmay open conceptual security holes. Failing fast may be better.mpv uses GPU APIs, which may be break on allocation errors (because driverauthors will have the same issues as described here), or don't even have a realconcept for dealing with OOM (OpenGL).libmpv is often used by GUIs, which I predict always break if OOM happens.Last but not least, OSes like Linux use "overcommit", which basically means thatyour program may crash any time OOM happens, even if it doesn't use malloc() atall!But still, don't just assume malloc() always succeeds. Use MP_HANDLE_OOM(). Theta* APIs do this for you. The reason for this is that dereferencing a NULLpointer can have security relevant consequences if large offsets are involved.Also, a clear error message is better than a random segfault.Some big memory allocations are checked anyway. For example, all code mustassume that allocating video frames or packets can fail. (The above exampleof dropping video frames during encoding is entirely possible in mpv.)Undefined behavior------------------Undefined behavior (UB) is a concept in the C language. C is famous for being alanguage that makes it almost impossible to write working code, becauseundefined behavior is so easily triggered, compilers will happily abuse it togenerate "faster" code, debugging tools will shout at you, and sometimes iteven means your code doesn't work.There is a lot of literature on this topic. Read it.(In C's defense, UB exists in other languages too, but since they're not usedfor low level infrastructure, and/or these languages are at times not rigorouslydefined, simply nobody cares. However, the C standard committee is still guiltyfor not addressing this. I'll admit that I can't even tell from the standard'sgibberish whether some specific behavior is UB or not. It's written like taxlaw.)In mpv, we generally try to avoid undefined behavior. For one, we want portableand reliable operation. But more importantly, we want clean output fromdebugging tools, in order to find real bugs more quickly and effectively.Avoid the "works in practice" argument. Once debugging tools come into play, orsimply when "in practice" stops being true, this will all get back to you in abad way.Global state, library safety----------------------------Mutable global state is when code uses global variables that are not read-only.This must be avoided in mpv. Always use context structs that the caller ofyour code needs to allocate, and whose pointers are passed to your functions.Library safety means that your code (or library) can be used by a librarywithout causing conflicts with other library users in the same process. To anypiece of code, a "safe" library's API can simply be used, without having toworry about other API users that may be around somewhere.Libraries are often not library safe, because they use global mutable stateor other "global" resources. Typical examples include use of signals, simpleglobal variables (like hsearch() in libc), or internal caches not protected bylocks.A surprisingly high number of libraries are not library safe because they needglobal initialization. Typically they provide an API function, which"initializes" the library, and which must be called before calling any otherAPI functions. Often, you are to provide global configuration parameters, whichcan change the behavior of the library. If two libraries A and B use library C,but A and B initialize C with different parameters, something "bad" may happen.In addition, these global initialization functions are often not thread-safe. Soif A and B try to initialize C at the same time (from different threads andwithout knowing about each other), it may cause undefined behavior. (libcurl isa good example of both of these issues. FFmpeg and some TLS libraries used to beaffected, but improved.)This is so bad because library A and B from the previous example most likelyhave no way to cooperate, because they're from different authors and have nobusiness knowing each others. They'd need a library D, which wraps library Cin a safe way. Unfortunately, typically something worse happens: libraries get"infected" by the unsafeness of its sub-libraries, and export a global init APIjust to initialize the sub-libraries. In the previous example, libraries A and Bwould export global init APIs just to init library C, even though the rest ofA/B are clean and library safe. (Again, libcurl is an example of this, if yousubtract other historic anti-features.)The main problem with library safety is that its lack propagates to alllibraries using the library.We require libmpv to be library safe. This is not really possible, because somelibraries are not library safe (FFmpeg, Xlib, partially ALSA). However, forideological reasons, there is no global init API, and best effort is made to tryto avoid problems.libmpv has some features that are not library safe, but which are disabled bydefault (such as terminal usage aka stdout, or JSON IPC blocking SIGPIPE forinternal convenience).A notable, very disgustingly library unsafe behavior of libmpv is callingabort() on some memory allocation failure. See error checking section.Logging-------All logging and terminal output in mpv goes through the functions and macrosprovided in common/msg.h. This is in part for library safety, and in part tomake sure users can silence all output, or to redirect the output elsewhere,like a log file or the internal console.lua script.Locking-------See generally available literature. In mpv, we use mp_thread for this.Always keep locking clean. Don't skip locking just because it will work "inpractice". (See undefined behavior section.) If your use case is simple, you mayuse C11 atomics, but most likely you will only hurt yourself and others.Always make clear which fields in a struct are protected by which lock. If afield is immutable, or simply not thread-safe (e.g. state for a single workerthread), document it as well.Internal mpv APIs are assumed to be not thread-safe by default. If they havespecial guarantees (such as being usable by more than one thread at a time),these should be explicitly documented.All internal mpv APIs must be free of global state. Even if a component is notthread-safe, multiple threads can use _different_ instances of it without anylocking.On a side note, recursive locks may seem convenient at first, but introduceadditional problems with condition variables and locking hierarchies. Theyshould be avoided.Locking hierarchy-----------------A simple way to avoid deadlocks with classic locking is to define a lockinghierarchy or lock order. If all threads acquire locks in the same order, nodeadlocks will happen.For example, a "leaf" lock is a lock that is below all other locks in thehierarchy. You can acquire it any time, as long as you don't acquire otherlocks while holding it.Unfortunately, C has no way to declare or check the lock order, so you should atleast document it.In addition, try to avoid exposing locks to the outside. Making the declarationof a lock private to a specific .c file (and _not_ exporting accessors orlock/unlock functions that manipulate the lock) is a good idea. Your component'sAPI may acquire internal locks, but should release them when returning. Keepingthe entire locking in a single file makes it easy to check it.Avoiding callback hell----------------------mpv code is separated in components, like the "frontend" (i.e. MPContext mpctx),VOs, AOs, demuxers, and more. The frontend usually calls "down" the usagehierarchy: mpctx almost on top, then things like vo/ao, and utility code on thevery bottom."Callback hell" is when components call both up and down the hierarchy,which for example leads to accidentally recursion, reentrancy problems, orlocking nightmares. This is avoided by (mostly) calling only down the hierarchy.Basically the call graph forms a DAG. The other direction is handled by eventqueues, wakeup callbacks, and similar mechanisms.Typically, a component provides an API, and does not know anything about itsuser. The API user (component higher in the hierarchy) polls the state of thelower component when needed.This also enforces some level of modularization, and with some luck the lockinghierarchy. (Basically, locks of lower components automatically become leaflocks.) Another positive effect is simpler memory management.(Also see e.g.: http://250bpm.com/blog:24)Wakeup callbacks----------------This is a common concept in mpv. Even the public API uses it. It's used when anAPI has internal threads (or otherwise triggers asynchronous events), but thecomponent call hierarchy needs to be kept. The wakeup callback is the onlyexception to the call hierarchy, and always calls up.For example, vo spawns a thread that the API user (the mpv frontend) does notneed to know about. vo simply provides a single-threaded API (or that looks likeone). This API needs a way to notify the API user of new events. But the voevent producer is on the vo thread - it can't simply invoke a callback back intothe API user, because then the API user has to deal with locking, despite notusing threads. In addition, this will probably cause problems like mentioned inthe "callback hell" section, especially lock order issues.The solution is the wakeup callback. It merely unblocks the API user fromwaiting, and the API user then uses the normal vo API to examine whether orwhich state changed. As a concept, it documents what a wakeup callback isallowed to do and what not, to avoid the aforementioned problems.Generally, you are not allowed to call any API from the wakeup callback. Youjust do whatever is needed to unblock your thread. For example, if it's waitingon a mutex/condition variable, acquire the mutex, set a change flag, signalthe condition variable, unlock, return. (This mutex must not be held whencalling the API. It must be a leaf lock.)Restricting the wakeup callback like this sidesteps any reentrancy issues andother complexities. The API implementation can simply hold internal (andnon-recursive) locks while invoking the wakeup callback.The API user still needs to deal with locking (probably), but there's only theneed to implement a single "receiver", that can handle the entire API of theused component. (Or multiple APIs - MPContext for example has only 1 wakeupcallback that handles all AOs, VOs, input, demuxers, and more. It simple re-runsthe playloop.)You could get something more advanced by turning this into a message queue. TheAPI would append a message to the queue, and the API user can read it. But thenyou still need a way to "wakeup" the API user (unless you force the API userto block on your API, which will make things inconvenient for the API user). Youalso need to worry about what happens if the message queue overruns (you eitherlose messages or have unbounded memory usage). In the mpv public API, thedistinction between message queue and wakeup callback is sort of blurry, becauseit does provide a message queue, but an additional wakeup callback, so APIusers are not required to call mpv_wait_event() with a high timeout.mpv itself prefers using wakeup callbacks over a generic event queue, becausemost times an event queue is not needed (or complicates things), and it isbetter to do it manually.(You could still abstract the API user side of wakeup callback handling, andavoid reimplementing it all the time. Although mp_dispatch_queue alreadyprovides mechanisms for this.)Condition variables-------------------They're used whenever a thread needs to wait for something, without nonsenselike sleep calls or busy waiting. mpv uses the mp_thread API for this.There's a lot of literature on condition variables, threading in general. Read it.For initial understanding, it may be helpful to know that condition variablesare not variables that signal a condition. mp_cond does not have anystate per-se. Maybe mp_cond would better be named mp_interrupt,because its sole purpose is to interrupt a thread waiting via mp_cond_wait()(or similar). The "something" in "waiting for something" can be calledpredicate (to avoid confusing it with "condition"). Consult literature for theproper terms.The very short version is...Shared declarations: mp_mutex lock; mp_cond cond_var; struct something state_var; // protected by lock, changes signaled by cond_varWaiter thread: mp_mutex_lock(&lock); // Wait for a change in state_var. We want to wait until predicate_fulfilled() // returns true. // Must be a loop for 2 reasons: // 1. cond_var may be associated with other conditions too // 2. mp_cond_wait() can have sporadic wakeups while (!predicate_fulfilled(&state_var)) { // This unlocks, waits for cond_var to be signaled, and then locks again. // The _whole_ point of cond_var is that unlocking and waiting for the // signal happens atomically. mp_cond_wait(&cond_var, &lock); } // Here you may react to the state change. The state cannot change // asynchronously as long as you still hold the lock (and didn't release // and reacquire it). // ... mp_mutex_unlock(&lock);Signaler thread: mp_mutex_lock(&lock); // Something changed. Update the shared variable with the new state. update_state(&state_var); // Notify that something changed. This will wake up the waiter thread if // it's blocked in mp_cond_wait(). If not, nothing happens. mp_cond_broadcast(&cond_var); // Fun fact: good implementations wake up the waiter only when the lock is // released, to reduce kernel scheduling overhead. mp_mutex_unlock(&lock);Some basic rules: 1. Always access your state under proper locking 2. Always check your predicate before every call to mp_cond_wait() (And don't call mp_cond_wait() if the predicate is fulfilled.) 3. Always call mp_cond_wait() in a loop (And only if your predicate failed without releasing the lock..) 4. Always call mp_cond_broadcast()/_signal() inside of its associated lockmpv sometimes violates rule 3, and leaves "retrying" (i.e. looping) to thecaller.Common pitfalls: - Thinking that mp_cond is some kind of semaphore, or holds any application state or the user predicate (it _only_ wakes up threads that are at the same time blocking on mp_cond_wait() and friends, nothing else) - Changing the predicate, but not updating all mp_cond_broadcast()/ _signal() calls correctly - Forgetting that mp_cond_wait() unlocks the lock (other threads can and must acquire the lock) - Holding multiple nested locks while trying to wait (=> deadlock, violates the lock order anyway) - Waiting for a predicate correctly, but unlocking/relocking before acting on it (unlocking allows arbitrary state changes) - Confusing which lock/condition var. is used to manage a bit of stateGenerally available literature probably has better examples and explanations.Using condition variables the proper way is generally preferred over using moremessy variants of them. (Just saying because on win32, "SetEvent" exists, andit's inferior to condition variables. Try to avoid the win32 primitives, even ifyou're dealing with Windows-only code.)Threads-------Threading should be conservatively used. Normally, mpv code pretends to besingle-threaded, and provides thread-unsafe APIs. Threads are used coarsely,and if you can avoid messing with threads, you should. For example, VOs and AOsdo not need to deal with threads normally, even though they run on separatethreads. The glue code "isolates" them from any threading issues.