diff --git a/doc/context.md b/doc/context.md
new file mode 100644
index 0000000000..73297f53aa
--- /dev/null
+++ b/doc/context.md
@@ -0,0 +1,308 @@
+# Introduction to contexts
+
+Like many C projects, FFmpeg has adopted the subset of
+[object-oriented programming](https://en.wikipedia.org/wiki/Object-oriented_programming)
+techniques that help solve its problems.  Object-like structures are called "contexts",
+and this document provides a general introduction to how they work.
+
+Object-oriented programming tends to focus on
+[access](https://en.wikipedia.org/wiki/Access_modifiers) as a primary concern.
+For example, members of a class are often designated "private" to indicate
+they are only accessible to code that's part of the class.  Less focus is put on
+[reflection](https://en.wikipedia.org/wiki/Reflection_(computer_programming)),
+where it is provided at all.  For example, C++ has no built-in way to serialize
+the state of an arbitrary object.
+
+Reflection is extremely important for FFmpeg, because user-facing
+options are implemented by reflecting the state of contexts.  Limiting
+access is a secondary concern, mainly important for ensuring
+implementation details can change between versions.
+
+Knowledge of object-oriented programming concepts can help when learning FFmpeg,
+but it's important not to fixate on FFmpeg's access control features,
+nor to overlook its reflection capabilities.  This document compares
+FFmpeg and OOP techniques where relevant, but does not require an understanding
+of object-oriented programming.
+
+## Example: modify text then print it
+
+The example below shows a context structure that receives input strings,
+modifies them in some context-dependant way, then prints them to a specified
+filehandle.
+
+```c
+/**
+ * Type information, accessible at runtime.
+ *
+ * Useful when configuring objects.
+ */
+enum ModifyThenPrintDialect {
+    MODIFY_THEN_PRINT_DIALECT_PLAIN_TEXT = 0,
+    MODIFY_THEN_PRINT_DIALECT_REGEX      = 1,
+    MODIFY_THEN_PRINT_DIALECT_REGEX_PCRE = 2
+};
+
+/**
+ * User-facing information about types
+ *
+ * Useful for describing contexts to the user.
+ */
+static const char *ModifyThenPrintDialectName[] = {
+    "plain text",
+    "regular expression",
+    "Perl-compatible regular expression"
+};
+
+/**
+ * Context for functions that modify strings before printing them
+ */
+struct ModifyThenPrintContext {
+
+    /**
+     * Information about the type of this particular instance
+     *
+     * Object-oriented programs would probably replace this with "sub-classes",
+     * where each class extends the base class to implement one dialect.
+     * But information about a type can also work more like "mixins",
+     * where several pieces of unrelated functionality are blended together
+     * to create an interface for a specific purpose.
+     */
+    enum ModifyThenPrintDialect dialect;
+
+    /**
+     * Internal context
+     *
+     * Contains anything that isn't part of the public interface.
+     * The most obvious OOP analogy is to "private" members that are only
+     * accessible to code that's part of the class.  But it could also contain
+     * information about "virtual functions" - functions that every sub-class
+     * guarantees to implement, but in their own class-specific way.
+     */
+    void *priv_data;
+
+    /**
+     * User-configurable options
+     *
+     * Best set through an API, but can be set directly if necessary
+     *
+     * Data from users needs to be validated before it's set, and the API
+     * might e.g. want to update some internal buffer for performance reasons.
+     * Setting these directly is always less robust than using an API,
+     * but might be worthwhile if you're willing to spend the time checking
+     * for edge cases, and don't mind your code misbehaving if future
+     * versions change the API but not the structure.
+     *
+     * Object-oriented programs would likely make these "protected" members,
+     * initialised in a constructor and accessed with getters and setters.
+     * That means code *related* to this class can access them directly,
+     * while unrelated code accesses them indirectly by calling functions.
+     *
+     * But the "protected access" analogy is quite limited. In particular,
+     * protected members don't have any special reflective properties,
+     * whereas FFmpeg options are usually configurable by end users.
+     */
+    char *replace_this;
+    char *with_this;
+
+    /**
+     * Programmer-configurable variable
+     *
+     * Object-oriented programs would represent this as a public member.
+     */
+    FILE *out;
+
+};
+
+/**
+ * Allocate and initialize a ModifyThenPrintContext
+ *
+ * Creates a new pointer, then fills in some sensible defaults.
+ *
+ * We can reasonably assume this function will initialise `priv_data`
+ * with a dialect-specific object, but shouldn't make any assumptions
+ * about what that object is.
+ *
+ * Object-oriented programs would likely implement this with "allocator"
+ * and "constructor" functions, (i.e. separate functions that reserve memory
+ * for an object and set its initial values).
+ */
+int ModifyThenPrintContext_alloc_context(struct ModifyThenPrintContext **ctx,
+                                         enum ModifyThenPrintDialect dialect,
+                                         FILE *out);
+
+/**
+ * Uninitialize and deallocate a ModifyThenPrintContext
+ *
+ * Does any work required by the internal context (e.g. deallocating
+ * `priv_data`), then deallocates the main context itself.
+ *
+ * Object-oriented programs would likely implement this with
+ * destructor and deallocator functions (i.e. separate functions
+ * that do the opposite of the allocator and constructor above).
+ */
+int ModifyThenPrintContext_free(struct ModifyThenPrintContext *ctx);
+
+/**
+ * Configure a ModifyThenPrintContext
+ *
+ * Checks that the arguments are valid in the context's dialect,
+ * then updates the options as necessary
+ *
+ * Object-oriented programs would likely represent this as a
+ * pair of "setter" functions (functions whose only job is to
+ * validate and update a value).
+ */
+int ModifyThenPrintContext_configure(struct ModifyThenPrintContext *ctx,
+                                     char *replace_this,
+                                     char *with_this);
+
+/**
+ * Print a single message
+ *
+ * Object-oriented programs would likely represent this with an ordinary
+ * member function (a function called like `context->function(...)`
+ * instead of `function( context, ... )`).
+ */
+int ModifyThenPrintContext_print(struct ModifyThenPrintContext *ctx,
+                                 char *msg);
+
+/**
+ * Print the contents of a ModifyThenPrintContext to a filehandle
+ *
+ * Provides human-readable information about keys and values.
+ *
+ * Object-oriented programs would likely represent this with some kind of
+ * `serialize` function (a function that iterates through all members,
+ * printing each one's name and contents to the file).  These can be member
+ * functions, but in practice many languages implement it some other way -
+ * an example of the lack of focus on reflection in object-oriented languages.
+ */
+int ModifyThenPrintContext_dump(struct ModifyThenPrintContext **ctx,
+                                FILE *dump_fh);
+
+/**
+ * How this context might be used in practice
+ */
+int print_hello_world()
+{
+
+    int ret = 0;
+
+    struct ModifyThenPrintContext *ctx;
+
+    if ( ModifyThenPrintContext_alloc_context( &ctx, MODIFY_THEN_PRINT_DIALECT_REGEX, stdout ) < 0 ) {
+        ret = -1;
+        goto EXIT_WITHOUT_CLEANUP;
+    }
+
+    if ( ModifyThenPrintContext_configure(ctx, "Hi|Hullo", "Hello") < 0 ) {
+        ret = -1;
+        goto FINISH;
+    }
+
+    if ( ModifyThenPrintContext_print( ctx, "Hi, world!\n" ) < 0 ) {
+        ret = -1;
+        goto FINISH;
+    }
+
+    if ( ModifyThenPrintContext_print( ctx, "Hullo, world!\n" ) < 0 ) {
+        ret = -1;
+        goto FINISH;
+    }
+
+    FINISH:
+    if ( ModifyThenPrintContext_free( ctx ) ) {
+        ret = -1;
+        goto EXIT_WITHOUT_CLEANUP;
+    }
+
+    EXIT_WITHOUT_CLEANUP:
+    return ret;
+
+}
+```
+
+## FFmpeg context structures
+
+The example above shows a generic context structure and its associated
+functions.  Some FFmpeg contexts are no more complex than that example,
+just as some objects are just key/value stores with some functions attached.
+But here are some examples to show the variety of contexts available in FFmpeg.
+
+AVHashContext presents a generic API for hashing data.  @ref hash.h
+"Its associated functions" show how to create, use and destroy a hash.
+The exact algorithm is specified by passing a string to av_hash_alloc(),
+and the list of strings can be retrieved from av_hash_names().
+Algorithm-specific internal context is stored in AVHashContext::ctx.
+
+AVCodecContext is a complex context representing data about an encoding or
+decoding session.  @ref avcodec.h "Its associated functions" provide an
+abstract interface to encode and decode data.  Like most widely-used contexts,
+its first member is an AVClass pointer containing instance-specific information.
+That means it's an *AVClass context structure* (discussed below).
+
+X264Context contains the internal context for an AVCodecContext that uses
+the x264 library.  @ref libx264.c "Its associated functions" provide a concrete
+implementation for interacting with libx264.  This class is not directly
+accessible through FFmpeg's public interface, so it's easier to change
+X264Context than to change AVCodecContext between releases.
+
+## Reflection with AVClass and AVOptions
+
+AVClass is a struct containing general information about a context.
+It's a generic version of `ModifyThenPrintDialect` in the example.
+An *AVClass context structure* is a context whose first member
+is an AVClass that reflects the context's interface.
+
+AVOption is a struct containing information about a member of a context,
+or of an internal context.  `replace_this` and `with_this` in the example
+would echa have an associated AVOption value stored in the context's AVClass.
+An *@ref avoptions "AVOptions"-enabled struct* is a structure which reflects
+its contents using the @ref avoptions "AVOptions" API.
+
+The terms *AVClass context structure* and *@ref avoptions "AVOptions"-enabled
+struct* have become synonymous in modern usage, although you might notice some
+distinction when looking at code written after AVClass was developed but
+before @ref avoptions "AVOptions" was added.
+
+Even though the name "AVClass" implies an analogy to an object-oriented
+base [class](https://en.wikipedia.org/wiki/Class_(computer_programming)), they
+behave more like [C++ concepts](https://en.wikipedia.org/wiki/Concepts_(C%2B%2B))
+or [Java interfaces](https://en.wikipedia.org/wiki/Interface_(Java)).  Unlike a
+base class, an AVClass doesn't imply any theoretical relationship between objects,
+and contexts of the same type will often have different AVClass values.  It's even
+theoretically possible for a single AVClass to be shared between contexts of
+different types.
+
+To understand how AVClass and @ref avoptions "AVOptions" work,
+consider the requirements for a `libx264` encoder:
+
+- it has to support common encoder options like "bitrate"
+- it has to support encoder-specific options like "profile"
+  - the exact options could change quickly if a legal ruling forces a change of backend
+- it must not expose implementation details (e.g. its encoding buffer)
+  - otherwise, trivial improvements would force an increase of the major API version
+- it has to provide useful feedback to users about unsupported options
+
+Common encoder options like "bitrate" need to be stored in AVCodecContext itself
+to avoid duplicating code, while encoder-specific options like "profile" have to
+be stored in the X264Context instance stored in AVCodecContext::priv_data.
+But both need to be presented to users (along with help text, default values etc.)
+in a way that makes it practical to build user interfaces to get and set those options.
+
+A context's AVClass contains a list of AVOption members, describing the
+user-configurable options in the class.  It also contains a tree of further AVClass
+members that represent internal context either for the current object or for any
+object of the same type that could potentially exist in the current version of FFmpeg.
+
+The @ref avoptions "AVOptions" API accepts any AVClass context structure,
+looks through its AVOption data, and uses that to examine, introspect, and modify
+the structure.  Because the API doesn't contain any type-specific information,
+it can be used to create a general user interface that adapts automatically
+when e.g. a new version of FFmpeg adds a new configuration option.
+
+## Summary
+
+FFmpeg uses "contexts" in ways that often resemble object-oriented programming.
+But it focuses less on encapsulation within arbitrarily complex systems,
+and more on providing reflectivity to make good user interfaces.
