मीडिया टेम्पलेट
iOS 26 च्या Now Playing व्ह्यूच्या स्टाइलमधील रिमोट मीडिया-प्लेयर कार्ड: कव्हर आर्ट, शीर्षक आणि कलाकार, पुशदरम्यान सरकत राहणारा स्क्रबर, आणि तुमचे स्वतःचे webhooks ट्रिगर करणारी मागील / प्ले-पॉज / पुढील बटणे — लॉक स्क्रीनवरून एक Sonos, टीव्ही किंवा Home Assistant मीडिया प्लेयर नियंत्रित करा.
हा विभाग App Store पुनरावलोकनाच्या प्रतीक्षेत असलेल्या ॲप अपडेटमधील एका वैशिष्ट्याचे वर्णन करतो. अपडेट थेट होताच ते इथे आपोआप अनलॉक होईल.
The media template turns a Live Activity into a remote for whatever is playing somewhere else -- a Sonos zone, the living-room TV, a Home Assistant media_player, a Jellyfin session. It draws like the iOS 26 Now Playing card: cover art, the track and artist, a scrubber that keeps moving on the device between your pushes, and a row of transport buttons. Every button fires your webhook; PushWard never talks to the player itself. Your side sends the state, the phone renders it, a tap comes back to you as an HTTP request, and your next push confirms what happened.
Requires PushWard 1.9.0 on the device. Older builds do not know the template and fall back to the generic layout: name, subtitle and a plain progress bar (the server mirrors position_seconds / duration_seconds into progress for exactly that case). Nothing breaks, the buttons simply are not there until the app updates.
Fields
The activity name is the source ("Living Room", "Kitchen TV") and stays on the card in small type; media_title is the big line.
| Field | Type | Description |
|---|---|---|
template | string | Required. Must be "media" |
media_title | string | Track, episode or programme title. Max 128 characters |
subtitle | string | Artist, show, album or channel -- the second line under the title |
playback_state | string | One of playing, paused, stopped, buffering. Default: paused. Only playing makes the bar tick; the play/pause glyph follows it too |
position_seconds | float | Playback position at the moment you sampled it. >= 0 |
duration_seconds | float | Total length. > 0, at most 604800 (7 days). Omit it for live or indeterminate media (radio, a stream): no bar is drawn and the elapsed clock still counts up |
position_at | integer | Unix timestamp (seconds) at which position_seconds was sampled. Defaults to the server's receive time, which is what you want when you push straight from the player. May be at most 300 s in the future. Re-send position_seconds on every play/pause transition so the device re-anchors instead of extrapolating from a stale sample |
volume | float | 0.0-1.0. Draws a thin volume bar between the volume buttons |
favorite | boolean | true fills the heart. Toggle it back on your next push after the favorite webhook lands |
controls | object | The transport buttons -- see Controls. All slots optional; no slots, no buttons |
image_url, image_shape, image_thumbhash | string | Cover art. The same trio as the generic template; the default shape is square. The device downloads the image, the server never does -- send the ThumbHash so the card has something to show the instant it appears |
icon | string | SF Symbol drawn where the art would be when there is none. Default: music.note |
accent_color | string | Named color or hex for the icon and the filled heart. Default: pink |
background_color, text_color | string | Color overrides, as on every template |
Sending any media field on another template is rejected with 422 rather than ignored, and switching an activity from media to another template clears them, so no card ends up carrying controls it cannot draw. url_action and secondary_url_action are accepted for compatibility but not rendered on the media card -- the transport row takes the button space; tap_action (tapping the card background) still works.
Controls
controls is an object of named slots. Each slot is an Action object -- the same shape as url_action, with the same limits on headers and body.
| Slot | Where it draws | Notes |
|---|---|---|
previous | Transport row | Skip back |
play_pause | Transport row (large, centre) | One toggle endpoint. The glyph shows pause while playback_state is playing and play otherwise |
play, pause | Transport row (same button) | Separate endpoints instead of a toggle. iOS picks pause while playing and play otherwise, and falls back to play_pause when the matching one is missing. Send whichever your player exposes; you can send all three |
next | Transport row | Skip forward |
stop | Transport row (small, trailing) | |
favorite | Transport row (small, leading) | Heart; filled when favorite: true |
volume_down, volume_up | Volume row | Speaker glyphs either side of the volume bar |
extra | Extras row | Array of up to 3 custom buttons (shuffle, repeat, a source switch). icon is required on each; title is the accessibility label |
What a tap does
- An
http(s)URL is always a silent webhook: the request is fired from the Live Activity, the app stays in the background, the response is discarded. The server fills inmethod: "POST"when you leave it out (stored and returned that way), and rejectsforeground: trueon a control with422-- a transport button that opens Safari is not a transport button. - A custom scheme (
spotify:,homeassistant://) opens that app instead. - Play/pause flips optimistically on the device the moment it is tapped, then your next push is the truth. Previous, next and stop dim their glyph until your next push arrives, so a slow player still reads as "working on it".
- Buttons exist on the Lock Screen and in the expanded Dynamic Island. The compact and minimal Island and the Apple Watch card are tap-to-open, except that the Watch shows one play/pause button.
Webhooks are best-effort and carry your credentials. They run inside the widget extension under its time limits, and headers / body ride on the push. Point them at a webhook endpoint with its own short-lived secret rather than a full API token, and keep them small: the whole activity has to fit the 4 KB APNs payload -- see Payload budget.
How the bar ticks
The device does not poll you. From position_seconds, position_at and duration_seconds it derives a start instant and, while playback_state is playing, animates the scrubber and both clocks (elapsed on the left, -remaining on the right) natively -- no update quota spent while a track plays.
playingwith a duration: the bar and clocks run fromposition_at - position_secondsuntil the end. Past the end they freeze at the end; they never wrap.playingwithout a duration: no bar, the elapsed clock counts up.paused,stopped,buffering: the bar and clocks freeze atposition_seconds.- An activity the server has marked stale (see
stale_ttl) freezes too, so a producer that died does not keep "playing". - Ending the activity forces the wire state to
stopped: an ended card never ticks.
Because the device extrapolates, every play/pause transition should carry the position you saw at that moment. Pause with {"playback_state": "paused", "position_seconds": 61.2}, resume with {"playback_state": "playing", "position_seconds": 61.2}. Leave position_at out and the server stamps the receive time; set it only to backdate a sample you took earlier. Position-only patches (position_seconds / position_at) go out at low push priority and coalesce; anything else on the card -- title, state, duration, volume, favorite, controls -- is a structural change and goes out at high priority.
Updates are merge patches
As on every template, a PATCH merges onto the stored content. Two things are specific to controls:
- The
controlsobject deep-merges: sending{"controls": {"stop": null}}removes the stop button and leaves the others alone; sending{"controls": {"next": {"url": "..."}}}replaces onlynext. controls.extrais an array and replaces wholesale, like every array field.- Re-sending a slot with a new
urland nomethodre-derives the method (POSTforhttp(s)): an explicit method from an earlier push is not remembered across a repoint, so send it again if you need something other than the default.titleis ignored on the fixed slots -- the glyph is the label -- onlyextrabuttons usetitleandicon.
Payload budget
The full example below is about 1.3 KB; eight controls each carrying a 40-character bearer header and a small body come to about 2.2 KB, well inside the 4 KB APNs limit. If a payload still does not fit, the server drops whole controls in this order until it does -- extra, then volume_down + volume_up, favorite, stop, then image_thumbhash, image_url, then previous + next -- and stops at the title, state, position and play slots. Headers and bodies are never stripped from a control that survives: a button that fires without its credentials and gets a 401 is worse than no button. What you GET back is always the full stored content; shedding only affects the push.
Surfaces
- Lock Screen -- art, title, subtitle, source, scrubber with both clocks, the transport row, then the volume row and extras when there is room (the extras row is hidden first, then the volume row, when the card would exceed the height iOS allows).
- Dynamic Island, expanded -- art leading, title and subtitle centred, elapsed trailing, scrubber and a compact transport row below. Buttons work here.
- Dynamic Island, compact -- art or icon leading; while playing the elapsed clock ticks trailing, otherwise a state glyph. Tap to open.
- Apple Watch -- art, title and one play/pause button. Mac and the in-app cards use the same layout as the Lock Screen.
Example: A Home Assistant player
Create the activity once, then push the player state. Every control here points at a Home Assistant webhook automation; the same shape works for anything that accepts an HTTP request.
curl -X POST https://api.pushward.app/activities \
-H "Authorization: Bearer hlk_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "living-room-player",
"name": "Living Room"
}'curl -X PATCH https://api.pushward.app/activities/living-room-player \
-H "Authorization: Bearer hlk_YOUR_TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{
"state": "ongoing",
"content": {
"template": "media",
"media_title": "Snooze",
"subtitle": "SZA",
"playback_state": "playing",
"position_seconds": 47.5,
"duration_seconds": 214,
"volume": 0.35,
"favorite": true,
"image_url": "https://cdn.example.com/art/snooze.jpg",
"image_shape": "square",
"image_thumbhash": "m8QNJRZ3d3B4aIh4iIiHd4iPjvcX",
"controls": {
"previous": { "url": "https://ha.example/api/webhook/pw-prev" },
"play_pause": { "url": "https://ha.example/api/webhook/pw-toggle" },
"next": { "url": "https://ha.example/api/webhook/pw-next" },
"stop": { "url": "https://ha.example/api/webhook/pw-stop" },
"favorite": { "url": "https://ha.example/api/webhook/pw-fav" },
"volume_down": { "url": "https://ha.example/api/webhook/pw-vol-down" },
"volume_up": { "url": "https://ha.example/api/webhook/pw-vol-up" },
"extra": [
{ "url": "https://ha.example/api/webhook/pw-shuffle", "icon": "shuffle", "title": "Shuffle" }
]
}
}
}'Then, as the player reports back (each one a merge patch onto the same activity):
{ "content": { "playback_state": "paused", "position_seconds": 61.2 } }
{ "content": { "playback_state": "playing", "position_seconds": 61.2 } }
{ "content": { "media_title": "Kill Bill", "position_seconds": 0, "duration_seconds": 153, "favorite": false } }
{ "state": "ended" }The one time position_at is worth sending is to backdate a sample you took a moment ago -- a poll that ran a few seconds before the push. The device starts the bar from that instant instead of from receipt:
{ "content": { "position_seconds": 61.2, "position_at": 1755500000 } }Typical Flow
- Start (
ended -> ongoing): send the track,playing, the position, the duration, the art and the controls - Transitions (
ongoing -> ongoing): on pause, resume, seek or track change send the newplayback_stateand the position you saw; on a webhook from a button, apply it to the player and confirm with the resulting state - Finish (
ongoing -> ended): end the activity when the player turns off or the queue runs out; the card stops ticking on its own
Use Cases
- Multi-room audio -- one activity per zone, buttons steer Sonos, HEOS or Music Assistant through Home Assistant
- The TV -- what is on, pause and skip from the Lock Screen without finding the remote
- Media servers -- a Jellyfin or Plex session with cover art and a stop button
- Radio and streams -- no duration, the elapsed clock counts up, favorite bookmarks the show