diff --git a/doc/context.md b/doc/context.md
new file mode 100644
index 0000000000..73caacf54f
--- /dev/null
+++ b/doc/context.md
@@ -0,0 +1,276 @@
+# Context-oriented programming
+
+Like many C projects, FFmpeg has adopted the subset of object-oriented 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 usually focusses on *access* as a primary concern.
+For example, members of an object are visibly designated "private", "constant"
+etc. to control how they are accessed.  *Reflection* is a secondary concern,
+where it is provided at all.  For example, C++ has no built-in way to get a
+string containing the name of a variable.
+
+Reflection is extremely important for FFmpeg, because user-facing options are
+implemented by reflecting state at runtime.  Limiting access is a secondary
+concern, mainly important for ensuring implementation details can change
+between versions.
+
+An object-oriented programmer learning FFmpeg should be careful not to fixate on
+FFmpeg's access control features, nor to overlook its reflection capabilities.
+Both are present, but have been given the level of focus appropriate for the task.
+
+## 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_PLAIN_TEXT = 0,
+    MODIFY_THEN_PRINT_REGEX      = 1,
+    MODIFY_THEN_PRINT_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 represent this example
+     * as a sub-class, but some instance-specific functionality
+     * behaves more like a mixin.
+     */
+    enum ModifyThenPrintDialect dialect;
+
+    /**
+     * Internal context
+     *
+     * Object-oriented programs would put private members in here,
+     * but could also use it to store a virtual function table
+     * or other "invisible" information.
+     */
+    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 when these change.
+     * Setting this 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.
+     * Making them user-configurable would be left to the programmer.
+     */
+    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
+ *
+ * This 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 represent this as an
+ * allocator function and a constructor.
+ */
+int ModifyThenPrintContext_alloc_context(struct ModifyThenPrintContext **ctx,
+                                         enum ModifyThenPrintDialect dialect,
+                                         FILE *out);
+
+/**
+ * Uninitialize and deallocate a ModifyThenPrintContext
+ *
+ * This does any work required by the internal context (e.g. deallocating
+ * `priv_data`), then deallocates the main context itself.
+ *
+ * Object-oriented programs would likely represent this as a
+ * destructor and a deallocator.
+ */
+int ModifyThenPrintContext_free(struct ModifyThenPrintContext *ctx);
+
+/**
+ * Configure a ModifyThenPrintContext
+ *
+ * This checks the arguments are valid in the context's dialect,
+ * then updates the options as necessary
+ *
+ * Object-oriented programs would likely represent this as a
+ * collection of setter functions.
+ */
+int ModifyThenPrintContext_configure(struct ModifyThenPrintContext *ctx,
+                                     char* replace_this,
+                                     char* with_this);
+
+/**
+ * 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
+ * `serialise` operation.
+ */
+int ModifyThenPrintContext_dump(struct ModifyThenPrintContext **ctx,
+                                FILE *dump_fh);
+
+/**
+ * Print a single message
+ *
+ * Object-oriented programs would likely represent this with a member function.
+ */
+int ModifyThenPrintContext_print(struct ModifyThenPrintContext *ctx,
+                                 char *msg);
+
+/**
+ * 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_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 is a generic example of a 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 member 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 in more detail 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 unlike AVCodecContext it can
+change significantly between releases.
+
+## Reflection with AVClass and AVOptions
+
+An *AVClass context structure* is a context whose first member is an AVClass
+that reflects its contents.  An *@ref avoptions "AVOptions"-enabled struct*
+is a structure which reflects its contents using the @ref avoptions "AVOptions"
+API.  These terms have become synonymous in modern usage, but you might notice
+some distinction when looking at code written after AVClass was developed but
+before @ref avoptions "AVOptions" was added.
+
+At first glance, an object-oriented programmer might be tempted to look at AVClass
+as a base class that contexts inherit from.  But 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.  A better
+analogy would be to [C++ concepts](https://en.wikipedia.org/wiki/Concepts_(C%2B%2B))
+or [Java interfaces](https://en.wikipedia.org/wiki/Interface_(Java)).
+
+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 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 easy to build user interfaces to get and set those options.
+
+A context's AVClass member contains a list of AVOption objects, describing
+the user-configurable members of the class.  It also contains functions to
+iterate through a tree of AVClass objects representing internal context either
+for the current object or for any class 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 are often resemble object-oriented programming.
+But it focuses less on encapsulation within arbitrarily complex systems,
+and more on providing reflectivity to make pleasant user interfaces.
