Skip to content

Loading media

MediaAddress, MediaLibrary and the source interface — how a reference becomes an asset, who owns what, and the release rule that has no automatic fallback.

All documentation — Reference
On this page

A step hands you a MediaRef. Turning that into a Sprite goes through three things: an address that has been resolved, a library that refcounts what comes back, and a source that actually loads it.

Everything here is in TaleShard.Unity.Media, and all of it is main thread only — nothing locks, and Unity’s asset APIs are main-thread-only anyway.

The shape of it

MediaRef → MediaAddress.From(…) → MediaLibrary.Sprite(…) → IMediaSource → Sprite
↑ ↑
MediaCatalog refcounts, and
(the index) releases nothing on its own

Bootstrap, first

MediaCatalog holds the project’s address index. Install it before anything asks for a picture.

MemberWhat it does
static bool IsInstalled { get; }
static IMediaCatalog Current { get; }Never null — with none installed it is an empty catalog that reports the missing bootstrap line the first time it is asked anything
static void Install(IMediaCatalog catalog)Last one wins
static void Install(IReadOnlyDictionary<string,string> keyById, IReadOnlyDictionary<string,string> keyByCode, IReadOnlyDictionary<string,string> extensionByKey)The generated maps directly — the one-line form
static void Clear()For tests, and for a project that swaps content at run time

IMediaCatalog itself is three members: KeyOf(idOrCode), ExtensionOf(key), Count. It is an interface rather than a direct reference because the generated MediaIndex lives in your assembly, and the reference between assemblies runs one way — your game references the plugin, never the reverse.

MediaAddress

readonly struct MediaAddress — a media reference that has been through the index.

The one place a media address is resolved, so a raw string can never reach a loader.

Resolution happens once, at construction. Afterwards a reference either carries an address or says why it does not.

Getting one

FactoryUse it for
From(MediaRef reference)The ordinary path — what a step’s fields, audio and cast all carry
From(MediaRef? reference)The nullable form, which is what every runtime accessor returns
From(string idOrCode)An id or a code, whichever you have. Ids are tried first.
FromCode(string code)When you have a code from somewhere the engine does not reach
Raw(string address)An address you assert — the way to put your own assets through the same refcounted library
Unresolved(string requested, string reason)A reference that resolves to nothing, carrying why

A bare From(null) is ambiguous between the overloads and will not compile. Cast the literal.

Reading one

MemberNotes
RequestedWhat the caller asked for, in the author’s own words. Kept even when resolution failed — it is the only thing a diagnostic can name.
KeyThe address: a relative path without its extension. Null when unresolved.
IsResolved
UnresolvedReasonWhy there is no Key. A default(MediaAddress) answers here too, naming itself.
IsAssertedDid you assert this with Raw, rather than the index deriving it?
ExtensionIncluding the dot, for a loader that needs the file on disk. Always empty for an asserted address.
RelativePathWith the extension — the same string the generated registry holds

ToString() reads as the address when resolved and as the failure when not, so a reference interpolated into a log line can never look like a working one.

Two addresses are equal when they resolve to the same address, not when they were asked for the same way. IsAsserted is not part of identity.

MediaLibrary

Where you ask for a picture, a sound or a video.

MediaLibrary.Shared is the one sources register themselves into. A library you construct yourself starts empty.

MemberNotes
static MediaLibrary Shared { get; }
IReadOnlyList<IMediaSource> Sources { get; }Highest priority first
int HeldCount { get; }Zero after a clean teardown
void Register(IMediaSource source)Ordered by priority immediately — registration order never decides anything
bool Unregister(IMediaSource source)Anything already loaded stays loaded
IMediaLoad<Sprite> Sprite(MediaAddress reference)
IMediaLoad<AudioClip> Audio(MediaAddress reference)
IMediaLoad<VideoClip> Video(MediaAddress reference)The clip only — playing it is a VideoPlayer’s job
void Release<T>(MediaAddress reference)
void ReleaseAll()
bool TryGet<T>(MediaAddress reference, out T asset)Peek without retaining
int RefCount<T>(MediaAddress reference)For a debug view
string Describe(MediaAddress reference)

A load is an IEnumerator, so a coroutine can yield return library.Sprite(reference); and then read Result.

TryGet takes no refcount, which is what makes it safe from a UI rebuild. It answers about now — false also means “still loading”.

Describe is the answer to “why is this picture blank”: which source would answer, and what the others would say. Print it from a debug window.

Registering a source that is already present — same type, same name, same priority — is a warned no-op rather than a duplicate.

IMediaSource

Somewhere media can be loaded from: Addressables, Resources, an asset bundle, a download cache, a test double.

MemberNotes
string Name { get; }For logs and Describe. A configurable source must say how here — see below.
int Priority { get; }Higher answers first. Ties resolve by name, and are reported once as a configuration mistake.
bool CanAnswer(MediaAddress reference)Structural only — no file system, no network, no asset lookup
string Describe(MediaAddress reference)What it would do, in one line. Allowed to say it would fail, and saying so is the point.
MediaLoad<Sprite> LoadSprite(MediaAddress reference)Never returns null
MediaLoad<AudioClip> LoadAudio(MediaAddress reference)Never returns null
MediaLoad<VideoClip> LoadVideo(MediaAddress reference)Never returns null

Three rules for writing one:

  • Priority is the whole ordering. Never registration order.
  • CanAnswer is asked on every load, so a source that does I/O there turns one load into two.
  • Report, never return a silent null. Every failure path calls Fail with a reason a developer can act on.

Name carries configuration because Register treats same type, name and priority as a duplicate — which is why the built-in resources source names its folder, as Resources:Media.

A load in progress

IMediaLoad is the untyped half — IsDone, Error, Reference — and it is an IEnumerator so a plain coroutine can wait on it.

IMediaLoad<T> adds Result, Ownership, and a Finished event.

Subscribing to an already-finished load invokes your handler immediately, so a caller never has to ask whether it missed the completion.

Check Error, not a null Result. “Not loaded yet”, “no such asset” and “the source is misconfigured” are three different answers, and a bare null is all three.

MediaLoad<T> is the implementation a source uses: create one, hand it back, complete it when your callback fires.

MemberNotes
MediaLoad(MediaAddress reference, string sourceName)
void Complete(T value, MediaOwnership ownership)Exactly once
void Complete(T value, MediaOwnership ownership, Action release)With how to let go of it
void Fail(string reason)Exactly once
static MediaLoad<T> Failed(reference, sourceName, reason)For a source refusing before it starts
Action ReleaseHandle { get; }Run by the library at refcount zero

A second Complete is refused and reported; the first answer stands.

MediaOwnership

Who owns the object, which decides whether releasing it destroys anything.

MemberMeaning
BorrowedThe object belongs to the source. Releasing means telling the source to let go.
CreatedThis load made it. Releasing must destroy it.

MediaPlan

What a thing needs, so you can preload it or report what is missing before a player sees a hole.

MethodReturns
Portraits(EnginePayload payload)Every entity’s portrait, across every definition
ForEntity(ResolvedEntity entity)One entity’s portrait plus every media its fields hold
ForStep(PlayStep step)Fields, choices’ content, audio, and every cast member’s portrait
ForBoard(EnginePayload payload, string boardId, string language = null)Every reference on every node, the board’s voice-over clips in that one language, plus the portraits of the entities those nodes point at

An unresolved reference is kept rather than dropped — that is the point, if you are auditing. Duplicates collapse by address, so one portrait shared by four characters is one load.

ForStep reads Fields rather than Presented, because a field with no text still carries a picture.

ForBoard takes the language you will play in, because a voice-over clip is per language and a board is planned before a session exists — pass the same value you passed to the player, which is a LanguageOption.Code. Omitting it means the same thing as passing the master language’s own code: both plan the master language’s clips, which is what an unlocalized build plays. It never plans every language at once — that is a whole extra copy of the dialogue audio.

The two built-in sources

Both self-register at BeforeSceneLoad. To use your own instead, either match type, name and priority — yours is then dropped as the duplicate — or unregister the built-in first.

ResourcesMediaSource

Loads through Resources.Load, and self-registers at FallbackPriority (−100) so a project always has at least one source.

new ResourcesMediaSource() reads the root of Resources. For media in a subfolder, name the part after it: new ResourcesMediaSource("Media") for Resources/Media.

CanAnswer is true for any resolved reference: it cannot know whether an asset is there without loading it, and answering “no” would hide it from Describe.

AddressablesMediaSource

No configuration: the address Addressables knows an asset by is the address MediaAddress.Key carries, because the plugin’s own postprocessor assigns it. Default priority 100, above the resources source, so a project that installed the package is asked here first regardless of registration order.

Everything it loads is Borrowed, and every load hands back the callback that releases the handle. Addressables refcounts internally too — a handle never released keeps its bundle resident for the life of the process.

Written for
Unity developers · Advanced integrators
Last updated