[← All NAPs](https://napplet.run/protocol/)

NAP-MEDIA / PR #10

# Media Session Control

Open PR

This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it.

**Shipped implementation: supported**

Session creation and playback binding have focused regression coverage; proposed torrent playback amendments are not implied.

Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/10), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change.

[Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html)

Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/7806f438530251fce5129f8e6239d6b642278759/naps/NAP-MEDIA.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/10/files).

## NAP-MEDIA

### Media Session Control

`draft`

**NAP ID:** NAP-MEDIA **Domain:** `media` **Depends:**

-   `resource` — capability · optional — `artwork.url` bytes are fetched via `resource.bytes` **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.media`; domain presence signals availability.

### Description

NAP-MEDIA provides media session management between napplets and the shell. Napplets that play audio or video need a way to expose playback state and metadata to the shell so the shell can display media controls (play/pause, skip, volume), aggregate multiple media sources across napplets, and enforce audio policy (e.g., mute on focus loss, duck overlapping sources). Napplets that cannot or should not fetch/play media directly need a way to request shell-owned playback without gaining direct network or audio authority.

The shell provides a media session registry and control surface. Napplets create sessions, report state and metadata, declare dynamic capabilities, and receive commands from the shell when they own playback. For shell-owned playback, the napplet passes a source reference and the shell validates policy, fetches, plays, owns lifecycle, and reports state back to the napplet. Each napplet may have multiple concurrent sessions (e.g., a music player with background playback and a notification sound effect).

### Ownership Model

Every session has a playback owner. The owner is the side that fetches or decodes the media, emits authoritative playback state, and receives playback commands.

-   `owner: "shell"` -- The napplet asks the shell to play a source. The shell validates policy, fetches through shell-controlled resource handling, owns the media element/backend, emits state, and may keep playback alive according to its own persistence policy. This is the strict sandbox path: the napplet does not gain direct media or network access.
-   `owner: "napplet"` -- The napplet plays media inside its own frame and registers the session so the shell can provide metadata display, media keys, audio focus, OS integration, and commands.

Stronger napplets can still choose shell-owned playback when they want shell policy enforcement, persistent playback, shared player UI, or OS-level integration.

### API Surface

| Operation | Parameters | Result | Wire |
| --- | --- | --- | --- |
| `createSession` | `options` (`MediaSessionCreate`) | `MediaSessionResult` | `media.session.create` / `media.session.create.result` |
| `updateSession` | `sessionId` (`text`), partial `metadata` (`MediaMetadata`) | none | `media.session.update` |
| `destroySession` | `sessionId` (`text`) | none | `media.session.destroy` |
| `reportState` | `sessionId` (`text`), `state` (`MediaState`) | none | `media.state` |
| `reportCapabilities` | `sessionId` (`text`), `actions` (list of `MediaAction`) | none | `media.capabilities` |
| `command` | `sessionId` (`text`), command (`MediaCommand`) | none | `media.command` |
| `onState` | `sessionId` (`text`), handler for `MediaState` | `Subscription` handle | `media.state` |
| `onCapabilities` | `sessionId` (`text`), handler for list of `MediaAction` | `Subscription` handle | `media.capabilities` |
| `onCommand` | `sessionId` (`text`), handler for `MediaCommand` | `Subscription` handle | `media.command` |
| `onControls` | `sessionId` (`text`), handler for list of `MediaAction` | `Subscription` handle | `media.controls` |
| `onEnded` | `sessionId` (`text`), handler for `MediaSessionEnded` | `Subscription` handle | `media.session.ended` |

#### Schemas

```cddl
MediaPlaybackOwner = "shell" / "napplet"
MediaAction = "play" / "pause" / "stop" / "next" / "prev" / "seek" / "volume"

MediaSessionCreate = {
  owner: MediaPlaybackOwner,
  ? sessionId: tstr,
  ? source: MediaSourceRef,
  ? metadata: MediaMetadata,
  ? context: MediaSessionContext,
  ? capabilities: [* MediaAction],
  ? autoplay: bool,
  ? live: bool,
}

MediaSourceRef = {
  ? url: tstr,
  ? blossomHash: tstr,
  ? nostr: MediaNostrRef,
  ? mimeType: tstr,
}

MediaNostrRef = {
  ? eventId: tstr,
  ? address: tstr,
  ? relays: [* tstr],
}

MediaArtwork = {
  ? url: tstr,
  ? hash: tstr,
}

MediaMetadata = {
  ? title: tstr,
  ? artist: tstr,
  ? album: tstr,
  ? artwork: MediaArtwork,
  ? duration: number,
  ? mediaType: "audio" / "video",
}

MediaSessionContext = {
  ? label: tstr,
  ? detail: tstr,
  ? index: uint,
  ? total: uint,
  ? links: [* MediaContextLink],
}

MediaContextLink = {
  rel: tstr,
  ? title: tstr,
  ? nostr: MediaNostrRef,
}
```

**Resource resolution.** The `artwork.url` field is a URL string. Napplets and shells that need the artwork bytes (for example, to render album art on a media controls surface) MUST fetch them through NAP-RESOURCE: `window.napplet.resource.bytes(url)`. The optional `artwork.hash` field, when present, MAY be used by shells as a content-addressed cache key but is not a substitute for the URL fetch — napplets address artwork by URL through the resource NAP. Artwork URLs do not grant direct network access. The web projection's runtime network policy governs external loads; an opaque sandbox origin alone does not block network requests. If the optional `resource` domain is unavailable, napplets SHOULD omit remote artwork or use local placeholders. Standard NAP-RESOURCE policy applies (private-IP block list at DNS-resolution time, MIME byte-sniffing, SVG rasterization, etc.).

`MediaState` fields:

| Field | Required | Type | Notes |
| --- | --- | --- | --- |
| `status` | yes | "playing", "paused", "stopped", or "buffering" | Playback state within an active session. |
| `position` | no | number | Playback position in seconds. |
| `duration` | no | number | Duration in seconds. |
| `volume` | no | number | Volume from 0.0 to 1.0. |
| `reason` | no | text | Cause of this transition; see Transition reasons. |

`MediaCommand` fields:

| Field | Required | Type | Notes |
| --- | --- | --- | --- |
| `action` | yes | `MediaAction` | Requested playback action. |
| `value` | no | number | Seek position or volume, as required by the action. |
| `reason` | no | text | Cause of the command; does not grant authority. |

`MediaSessionEnded` fields:

| Field | Required | Type | Notes |
| --- | --- | --- | --- |
| `sessionId` | yes | text | Canonical identifier of the retired session. |
| `reason` | yes | text | Why the shell retired the session. |

`MediaSessionResult` fields:

| Field | Required | Type | Notes |
| --- | --- | --- | --- |
| `sessionId` | conditional | text | Required on successful creation. |
| `owner` | conditional | `MediaPlaybackOwner` | Required on successful creation. |
| `error` | no | text | Creation failure; no session was created. |

**`createSession(options)`** -- Creates a new media session. The napplet MUST set `owner` to `"shell"` or `"napplet"`. For shell-owned sessions, `source` is required and the shell fetches/plays it. For napplet-owned sessions, `source` is optional and advisory; the napplet owns playback inside its frame. The napplet MAY provide a preferred `sessionId`, metadata, context, initial capabilities, `autoplay`, and `live`. Returns the shell-canonical `sessionId` and owner. The shell MAY reject session creation (e.g., invalid source, unsupported owner mode, or session limit exceeded).

**`updateSession(sessionId, metadata)`** -- Updates metadata for an existing session. Partial updates are supported -- only the fields provided are changed. Fire-and-forget.

**`destroySession(sessionId)`** -- Requests session retirement. The shell removes it from the media control surface and sends `media.session.ended` with reason `destroyed`. The request is fire-and-forget; the terminal notification has no request correlation id.

**`reportState(sessionId, state)`** -- Reports the current playback state for a napplet-owned session. The playback owner sends state whenever it changes, including transition reasons when known (see Transition reasons). Fire-and-forget, high frequency during active playback.

**`reportCapabilities(sessionId, actions)`** -- Declares which media actions a napplet-owned session currently supports. Capabilities are dynamic -- a streaming source may not support `seek` initially but add it once buffered. The shell uses this to enable/disable media control buttons. Fire-and-forget.

**`onCommand(sessionId, callback)`** -- Listens for media commands from the shell for a napplet-owned session. The shell sends commands based on user interaction or audio policy; `reason` distinguishes their causes. The `value` parameter is used for `seek` (position in seconds) and `volume` (0.0 to 1.0). Returns a Subscription with `close()`.

**`onControls(sessionId, callback)`** -- Listens for the shell's control list. The shell tells the napplet which controls the shell supports, so the napplet can adapt its own UI (e.g., hide a next/prev button if the shell does not support it). Returns a Subscription with `close()`.

**`command(sessionId, command)`** — Requests an allowed action on shell-owned playback. It cannot revive a retired session or change playback ownership.

**`onState(sessionId, callback)`** and **`onCapabilities(sessionId, callback)`** — Receive authoritative state and supported actions for shell-owned playback.

**`onEnded(sessionId, callback)`** — Receives terminal retirement for either owner mode. The binding MUST retain the terminal record until the napplet endpoint is destroyed. A handler registered after retirement MUST receive it once. Retirement MUST stop further state, capability, control, and command callbacks for that session. A napplet-owned player MUST stop its output and release session resources when it receives retirement.

### Wire Protocol

`media.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`).

| Type | Direction | Payload fields |
| --- | --- | --- |
| `media.session.create` | napplet -> shell | `id`, `owner`, `sessionId?`, `source?`, `metadata?`, `context?`, `capabilities?`, `autoplay?`, `live?` |
| `media.session.create.result` | shell -> napplet | `id`, `sessionId?`, `owner?`, `error?` |
| `media.session.update` | napplet -> shell | `sessionId`, `metadata` |
| `media.session.destroy` | napplet -> shell | `sessionId` |
| `media.session.ended` | shell -> napplet | `sessionId`, `reason` |
| `media.state` | owner -> peer | `sessionId`, `status`, `position?`, `duration?`, `volume?`, `reason?` |
| `media.capabilities` | owner -> peer | `sessionId`, `actions` |
| `media.command` | controller -> owner | `sessionId`, `action`, `value?`, `reason?` |
| `media.controls` | shell -> napplet | `sessionId`, `controls` |

Key design notes:

-   `media.session.create` / `media.session.create.result` use `id` for correlation. The napplet MAY provide `sessionId` as a preferred client identifier, but the shell canonicalizes the session id in `media.session.create.result`.
-   `owner` is required. Without it, the shell cannot know who fetches media, emits state, receives commands, or owns audio output.
-   `source` is required for shell-owned sessions and optional for napplet-owned sessions. A napplet-owned `source` is advisory metadata; it does not ask the shell to fetch or play media.
-   `context` is optional session association metadata. It can describe where this media item sits in a list (`label`, `detail`, `index`, `total`) and can link related resources such as a Nostr event, address, or live-chat target. Context does not grant fetch authority and MUST NOT change session ownership.
-   `media.session.update`, `media.session.destroy`, `media.state`, and `media.capabilities` are fire-and-forget (no `id` correlation).
-   For napplet-owned sessions, `media.state` and `media.capabilities` are napplet -> shell, and `media.command` is shell -> napplet.
-   For shell-owned sessions, `media.state` and `media.capabilities` are shell -> napplet, and `media.command` is napplet -> shell when the napplet requests an allowed playback action.
-   `media.session.ended` is a shell-initiated terminal notification with no `id` correlation.
-   `media.controls` is shell-initiated. The shell pushes its supported control list so the napplet can adapt its UI.
-   Multiple sessions per napplet are supported. Each session is identified by `sessionId`.
-   Volume handling is owner-specific. For napplet-owned sessions, the napplet reports its own volume via `media.state`, and the shell can request volume changes via `media.command` with `action: 'volume'`. The effective volume may be the product of napplet volume and shell policy volume. For shell-owned sessions, the shell owns output volume and reports it in `media.state`.

#### Session ID Canonicalization

The napplet MAY include `sessionId` in `media.session.create` as a stable client-generated hint. The shell MUST return the canonical `sessionId` in `media.session.create.result`. The canonical id MAY equal the napplet-supplied value, or it MAY be rewritten to avoid collisions, enforce namespace policy, or bind the id to the napplet identity.

All later messages MUST use the canonical `sessionId`. Messages that use the pre-canonical hint after creation SHOULD be treated as unknown-session messages.

#### Transition reasons

`reason` explains a transition or command; it does not determine session existence. Only `media.session.ended` retires a session. Missing or unrecognized reasons MUST be treated as unspecified causes, while still processing the message's status, action, or terminal meaning. Receivers MUST NOT infer natural completion or permission to resume from `stopped` or `paused` alone.

| Reason | Meaning |
| --- | --- |
| `user` | The user acted in the shell, player, or napplet UI. |
| `ended` | The current source completed naturally. |
| `error` | Playback failed. |
| `preempted` | Another session permanently took this session's output. |
| `focus-lost` | The shell or platform temporarily withdrew audio focus. |
| `focus-gained` | The shell permits playback to resume after focus returns. |
| `destroyed` | The napplet requested session destruction. |
| `backend-exited` | The playback backend exited without a more specific known cause. |
| `policy` | The shell revoked the session under its policy. |

Playback owners SHOULD include the known reason on state transitions. A natural completion that leaves the session usable MUST be reported as `status: "stopped"` with `reason: "ended"`; user stop and playback failure use `user` and `error` respectively when known. An ended track does not by itself retire the session. If the shell retires the session for that completion instead, it MUST send `media.session.ended` with reason `ended` as the sole notification of that completion. Consumers MUST NOT interpret other reasons as natural completion.

For an active session, a later `play` remains subject to advertised capabilities and shell policy. An empty capability list disables actions; it MUST NOT be used as a substitute for session retirement.

#### Session retirement and preemption

The shell MAY retire either owner mode under policy. It MUST retire a session when it permanently reallocates that session's output to another session, or when its playback backend exits and it will not preserve a usable session. Permanent output replacement uses reason `preempted`. Backend exit uses the known cause (`user`, `ended`, or `error`), or `backend-exited` when unknown.

Before notifying retirement, the shell MUST remove the session from dispatch and stop or revoke its output authority. It MUST send exactly one `media.session.ended` for that session to its still-live napplet endpoint. Any already-enqueued state, capability, control, or command messages MUST precede the terminal notification. The shell MUST NOT send further state, capabilities, controls, or commands for the retired session. The binding MUST discard late messages for it and retain the terminal record for `onEnded`.

For a newly created session, a successful creation result MUST precede any terminal notification. Failed creation produces no retirement notification. Retired identifiers MUST NOT be reused during the napplet endpoint's lifetime, even when a later creation requests the same preferred identifier.

Messages targeting retired sessions, including `play`, MUST be silently ignored as unknown-session messages. They MUST NOT reacquire output or preempt another session. Playing again requires successful creation of a new session. The shell MUST continue enforcing consent, autoplay, and audio-focus policy on creation.

Endpoint destruction requires cleanup but no notification to the dead endpoint. Shell-promoted persistent playback MUST first detach from the destroyed endpoint; its former session identifier MUST NOT become usable by a replacement endpoint.

#### Temporary audio-focus loss

Temporary focus loss keeps the session alive. For shell-owned playback, the shell MUST report a focus-induced pause as `media.state` with `status: "paused"` and `reason: "focus-lost"`. For napplet-owned playback, the shell MUST identify a focus-induced pause command with `reason: "focus-lost"`. The napplet MUST pause and report its actual state. The reason does not transfer playback ownership. A napplet-owned session MUST support `pause` if it produces pausable output; otherwise the shell MUST revoke output and retire the session under policy.

A napplet MUST NOT resume automatically merely because its window regains focus. For napplet-owned playback, automatic resumption requires a shell `play` command with `reason: "focus-gained"`. The shell MAY issue that command only when the session was playing before the focus-induced pause and no later user pause, stop, retirement, or playback failure superseded it. The napplet MUST ignore that automatic-resume command if its own subsequent user action or playback failure superseded the focus pause. For shell-owned playback, the shell applies the same conditions and reports resumed playback with reason `focus-gained`. User-requested play remains subject to ordinary shell policy. A focus transition MUST NOT revive a retired session.

#### Source References

`source` identifies media the shell may play for a shell-owned session. It supports direct URLs, Blossom content hashes, and Nostr event/address references. Shells MAY support a subset of source forms and reject unsupported forms during session creation.

For `owner: "shell"`, `source` is required unless the shell defines a separate out-of-band source binding for the napplet. The shell MUST fetch and validate source bytes through shell-controlled policy. The napplet does not gain direct network access.

For `owner: "napplet"`, `source` is optional metadata. The shell MUST NOT treat a napplet-owned `source` as permission to fetch or play media on the napplet's behalf.

#### Session Context

`context` describes the media session around the source. It is for UI, queue position, and related-resource affordances. A shell MAY ignore any context field it does not understand.

`context.links[]` entries identify related resources by relation name. Relation names are lower-case strings. Implementations MAY define relation names such as `live-chat` or `source-event`, but unknown relation names MUST be ignored.

Nostr links use `MediaNostrRef`. `nostr.address` MAY be an address coordinate such as `30311:<pubkey>:<d-tag>`. `nostr.eventId` MAY identify an event. `nostr.relays` are optional routing hints. Absence of `nostr.relays` MUST NOT make the link invalid; the runtime resolves the Nostr reference through its normal routing policy.

#### Metadata

All metadata fields are optional. The `artwork` field supports two forms:

-   `url` -- A direct URL to the artwork image.
-   `hash` -- A Blossom hash (SHA-256) that the shell can resolve via its configured Blossom servers. If both are provided, the shell MAY prefer either.

```json
{
  "title": "Song Title",
  "artist": "Artist Name",
  "album": "Album Name",
  "artwork": { "url": "https://example.com/cover.jpg", "hash": "abc123..." },
  "duration": 240,
  "mediaType": "audio"
}
```

#### Examples

**Create a session:**

```
-> { "type": "media.session.create", "id": "m1", "owner": "napplet", "sessionId": "s1", "metadata": { "title": "My Song", "artist": "The Artist" } }
<- { "type": "media.session.create.result", "id": "m1", "sessionId": "s1", "owner": "napplet" }
```

**Create a shell-owned playback session:**

```
-> { "type": "media.session.create", "id": "m2", "owner": "shell", "source": { "url": "https://example.com/live.mp3", "mimeType": "audio/mpeg" }, "metadata": { "title": "Live Stream" }, "autoplay": true, "live": true }
<- { "type": "media.session.create.result", "id": "m2", "sessionId": "shell-7", "owner": "shell" }
```

**Create a shell-owned live stream with a chat association:**

```
-> { "type": "media.session.create", "id": "m3", "owner": "shell", "source": { "url": "https://example.com/live.mp3", "mimeType": "audio/mpeg" }, "metadata": { "title": "Live Stream" }, "context": { "links": [{ "rel": "live-chat", "title": "Live Stream", "nostr": { "address": "30311:pubkey:d-tag" } }] }, "autoplay": true, "live": true }
<- { "type": "media.session.create.result", "id": "m3", "sessionId": "shell-8", "owner": "shell" }
```

**Update metadata:**

```
-> { "type": "media.session.update", "sessionId": "s1", "metadata": { "title": "Updated Title" } }
```

**Report playback state:**

```
-> { "type": "media.state", "sessionId": "s1", "status": "playing", "position": 42.5, "duration": 240, "volume": 0.8 }
```

**Report capabilities:**

```
-> { "type": "media.capabilities", "sessionId": "s1", "actions": ["play", "pause", "seek", "volume"] }
```

**Shell sends a command:**

```
<- { "type": "media.command", "sessionId": "s1", "action": "pause" }
<- { "type": "media.command", "sessionId": "s1", "action": "seek", "value": 120 }
<- { "type": "media.command", "sessionId": "s1", "action": "volume", "value": 0.5 }
```

**Napplet requests a command on shell-owned playback:**

```
-> { "type": "media.command", "sessionId": "shell-7", "action": "pause" }
```

**Shell reports state for shell-owned playback:**

```
<- { "type": "media.state", "sessionId": "shell-7", "status": "playing", "position": 12.0, "volume": 0.7 }
```

**Shell sends control list:**

```
<- { "type": "media.controls", "sessionId": "s1", "controls": ["play", "pause", "stop", "next", "prev", "seek", "volume"] }
```

**Destroy a session:**

```
-> { "type": "media.session.destroy", "sessionId": "s1" }
<- { "type": "media.session.ended", "sessionId": "s1", "reason": "destroyed" }
```

**Track completes; the session remains usable:**

```
<- { "type": "media.state", "sessionId": "shell-7", "status": "stopped", "reason": "ended" }
```

**Another session takes the shared player; the old session is retired:**

```
<- { "type": "media.session.ended", "sessionId": "shell-7", "reason": "preempted" }
-> { "type": "media.command", "sessionId": "shell-7", "action": "play" }
```

The stale command is silently ignored. A new session is required to play again.

**Temporary focus loss and permitted resumption of napplet-owned playback:**

```
<- { "type": "media.command", "sessionId": "s2", "action": "pause", "reason": "focus-lost" }
-> { "type": "media.state", "sessionId": "s2", "status": "paused", "reason": "focus-lost" }
<- { "type": "media.command", "sessionId": "s2", "action": "play", "reason": "focus-gained" }
```

The resume command is valid only if no intervening user action or failure superseded the focus pause.

**Session creation rejected:**

```
<- { "type": "media.session.create.result", "id": "m1", "sessionId": "s1", "error": "session limit exceeded" }
```

#### Error Handling

`media.session.create.result` includes an `error` field (string) on failure. Common errors: `"session limit exceeded"`, `"invalid session ID"`. When `error` is present, the session was not created.

For shell-owned sessions, common creation errors also include `"missing source"`, `"unsupported source"`, `"source blocked"`, and `"autoplay denied"`.

Messages referencing an unknown `sessionId` (e.g., `media.state` for a destroyed session) MUST be silently ignored by both napplet and shell.

Commands that are not valid for the current owner mode, source, or capabilities SHOULD be silently ignored or rejected using an implementation-defined diagnostic channel. They MUST NOT cause a session to switch owner.

### Shell Behavior

-   The shell MUST accept `media.session.create` messages and respond with `media.session.create.result` carrying the same `id`.
-   The shell MUST reject `media.session.create` messages without `owner`.
-   The shell MUST reject `owner: "shell"` creation when `source` is missing or blocked by policy.
-   The shell MUST return the canonical `sessionId` in `media.session.create.result`.
-   The shell MUST track active sessions per napplet and apply Session retirement and preemption on destruction, output replacement, and terminal backend exit.
-   The shell SHOULD display media controls (play/pause, skip, volume) for active sessions based on the napplet's reported capabilities.
-   The shell SHOULD update its media control UI in response to `media.state` messages.
-   The shell MAY send `media.command` messages to control napplet playback based on user interaction with the shell's media controls.
-   The shell MAY send `media.controls` to inform the napplet which controls the shell supports. The napplet can adapt its own UI accordingly.
-   The shell MAY enforce audio policy (e.g., mute sessions on focus loss, duck overlapping sources, enforce a single-active-output policy).
-   The shell SHOULD make audio focus decisions across all sessions, regardless of owner.
-   For shell-owned sessions, the shell MUST own fetching, playback lifecycle, state emission, and policy enforcement.
-   For napplet-owned sessions, the shell MUST NOT fetch/play `source` on the napplet's behalf unless the napplet creates a separate shell-owned session.
-   When the owning napplet/window closes, the shell MUST destroy napplet-owned sessions and SHOULD stop shell-owned sessions unless the shell explicitly promotes the session to persistent playback.
-   If shell-promoted persistent playback is allowed, the shell MUST detach the persistent session from the closed iframe before destroying the napplet's message endpoint, and MUST define how the user can stop or revoke that playback.
-   The shell MAY limit the number of concurrent sessions per napplet.
-   The shell MAY enforce ACL checks on `media` capabilities (e.g., restricting which napplets can create media sessions).
-   The shell MUST silently ignore messages from unknown session IDs.
-   On endpoint removal, the shell MUST clean up all sessions still bound to it. Explicitly detached persistent playback follows the persistence policy above.

### Command Validity

For napplet-owned sessions:

-   The shell SHOULD only send commands listed in the napplet's current `media.capabilities`. Focus withdrawal follows Temporary audio-focus loss; unsupported pause MUST NOT leave revoked output playing.
-   The napplet SHOULD ignore commands it does not currently support.
-   `seek` MUST include `value` as a position in seconds.
-   `volume` MUST include `value` from `0.0` to `1.0`.

For shell-owned sessions:

-   The napplet MAY send `media.command` only for actions exposed by the shell through `media.controls` or `media.capabilities`.
-   The shell MUST enforce source policy, autoplay policy, audio focus, and output volume.
-   The shell MAY ignore `seek` when the source is live or not seekable.
-   The shell MAY ignore `next` and `prev` unless the source represents a playlist or queue.
-   The shell MUST NOT expose raw source bytes to the napplet as part of command handling.

### Security Considerations

-   Media sessions expose metadata and playback state to the shell. Napplets SHOULD NOT include sensitive information in metadata fields.
-   Session context is napplet-supplied metadata. Shells SHOULD validate Nostr references before using them for UI or routing and MUST ignore unknown context links.
-   Artwork URLs are fetched by the shell, not the napplet (the napplet is sandboxed with no network access). The shell SHOULD validate URLs and enforce a content security policy for fetched resources.
-   Shell-owned `source` URLs are fetched and played by the shell, not the napplet. The shell MUST apply the same resource safety policy it applies to artwork and other external bytes.
-   Blossom artwork hashes allow the shell to resolve artwork through its own Blossom infrastructure without the napplet needing network access. The shell controls which Blossom servers are queried.
-   For napplet-owned sessions, volume control is advisory -- the napplet controls actual audio output. A malicious napplet could ignore volume commands. The shell MAY enforce volume limits at the iframe level using the Web Audio API or iframe attribute policies if available. For shell-owned sessions, the shell controls actual output volume.
-   Session creation is rate-limited by the shell. A napplet that creates excessive sessions can be throttled or denied.
-   Transition reasons are explanatory metadata, not authorization. The shell MUST NOT let a caller-supplied reason bypass ownership, capability, autoplay, or audio-focus checks.
-   The `media.command` message allows the shell to control napplet behavior. The napplet trusts the shell ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) security model) but SHOULD validate the `action` against its declared capabilities.

### Implementations

-   (none yet)

### Changelog

-   `d3adc87` - Introduced NAP-MEDIA for media session control.
    
-   `7eeac9f` - Added media session context associations for linking playback state to external context.
    
-   `df41999` - Adopted injected-domain availability and clarified mediated artwork loading and optional-resource fallback.
    
-   `bd521c6` - Added playback and command reasons, terminal session notifications, stale-command rejection, ordered retirement delivery, and audio-focus resumption rules; aligned the operations table with the wire contract.
    

[Join the discussion · PR #10 ↗](https://github.com/napplet/naps/pull/10)[Read proposed changes ↗](https://github.com/napplet/naps/pull/10/files)

On this pageSpecification metadata

Capability domain

`media`

Repository state

Open PR

Registry status

Draft

Document marker

draft

Declared dependencies

-   [resource](https://napplet.run/protocol/nap-resource-pr-80/)

Source revision

`7806f4385302`

[View source on GitHub ↗](https://github.com/napplet/naps/pull/10/files)

## Also under this NAP

-   [Media Session Control Draft PR · #133 →](https://napplet.run/protocol/nap-media-pr-133/)
