Smooth mascot playback
For one animation that never changes, a single looping video player is enough. If a loop restart clears the display during a seek, use the same two-player handoff for that repeat as well. For a mascot that changes clips, use two persistent players and keep the current picture visible until the next player has a decoded frame. This guide describes how to implement that in your app, without a Masko SDK dependency.
An interactive mascot also needs a graph controller. The controller chooses the next authored edge from app inputs, conditions and priorities. The player handles media preparation and presentation. Two players alone do not implement the graph.
The handoff
Call the visible slot A and the spare slot B. The names switch after each handoff.
- Keep A mounted and visible. Do not change its source, clear its surface or remove its last frame while B prepares.
- Load the next clip into B. Keep both players at the same size, crop, anchor and scale. Prepare the correct start position and decode a frame. If preparing early, pause B on its prepared frame so the animation is not consumed unseen.
- Wait for both conditions: B has a verified frame, and the graph allows the transition now. A still frame is preferable to showing an empty B.
- Reveal B and hide A in one presentation update, with no opacity animation. On a canvas or GPU surface, draw only B's verified frame in that update.
- Mark B as the selected slot, then pause A. Wait for replacement presentation before clearing or releasing A's media. A React render or a load event is not that confirmation. Retire A only if it has not been reused for a newer request.
- Notify the graph that the requested clip is visible. Advance to its destination when the current clip actually ends, respecting loop and return-path rules.
Do not crossfade the players. With transparent characters, overlapping two frames can show two mascots or a ghost outline. An atomic handoff is appropriate when the outgoing pose and incoming pose match. If the artwork itself jumps in position or pose, fix the asset alignment or use the authored transition between those poses.
Prevent stale callbacks from switching the picture
Track the visible slot separately from the requested slot. Assign a new request ID and slot version whenever an item is replaced, even when the URL is unchanged. Ready, error, end, seek and cleanup callbacks must check that identity.
For example, if B is loading a celebration and a newer request replaces it with a wave, the celebration's late ready callback must not reveal B or complete the wave. Cancel its observers, frame callbacks and preparation task. An old cleanup callback must not empty a slot that now contains a different clip.
The following is control-flow pseudocode. The preparation and presentation functions belong to your application; they are not Masko API or SDK methods.
requestClip(clip):
cancel pending preparation and its callbacks
token = a new request ID
candidate = the slot that is not visible
candidate.version += 1
pending = { token, candidate, candidate.version, clip }
prepare candidate at clip's start position
keep visible slot playing, or hold its final frame if it ends
onCandidateFrame(token, version, frame):
ignore if request or slot version no longer matches
store this verified frame; pause candidate if handoff is not yet allowed
tryCommit()
onGraphAllowsHandoff():
tryCommit()
tryCommit():
require current pending request, verified frame and allowed graph boundary
require foreground playback and no failure or unfinished seek
in one presentation update:
present candidate's verified frame and start/resume its playback
record candidate as visible
pause old slot
on replacement presentation, emit clip-visible for this request
then clean up old slot only if its version is unchanged and it is still unused
onTimeoutOrFailure(token):
ignore stale requests
cancel this preparation, keep the visible frame, report failure
retry a supported source or refresh expired delivery without changing visibilityA timeout is a failure to prepare, not proof that B is ready. Never implement “wait 500 ms, then switch anyway.” On first launch, keep a poster visible until the first video frame is ready. If autoplay is blocked, keep the poster/current frame and offer a play gesture.
HTML and React
Keep two video elements alive for the lifetime of the player. React should not key or remount the player by clip URL, or update visibility through separate renders that briefly hide both slots. Use refs for player ownership, pending request identity and frame callbacks; React state can report progress to the UI.
Use muted, playsInline and appropriate preload settings. preload="auto" is
only a loading hint. loadedmetadata, canplay, or a resolved play() promise
alone must not trigger the handoff. Use a current-source
requestVideoFrameCallback
to observe a frame sent to the compositor. Register the callback before starting
B, and invalidate it if the source or seek changes. This callback is a readiness
signal, not a guarantee of perfect presentation timing on every browser.
A useful layout is two decoder videos feeding one persistent visible canvas. Prepare the candidate frame in a staging surface, then draw the selected frame into the visible canvas. Retain the last good drawing until its replacement is ready; never clear the visible canvas merely because loading started or a frame callback is late. Keep transparent alpha and identical drawing dimensions.
Alternatively, use two fixed-position video layers with an atomic visibility
swap. Do not use display:none or detach the preparing element and assume it
will still produce compositor callbacks. Test decoding with the actual browser
and chosen layout. Configure cross-origin media access before assigning src
and verify the media server's CORS support when using canvas.
If frame callbacks are unavailable or the preparing surface cannot decode in that browser, keep the current frame/poster and report the limitation. Do not replace frame evidence with an arbitrary delay. A fallback rendering path needs its own playback verification before claiming the same continuity.
Choose the available transparent WebM or HEVC media based on tested playback support. Codec support does not by itself prove alpha support. If a source fails, try an available supported alternative in the spare slot. See media formats. Cancel frame callbacks, event listeners and pending loads on disposal; pause when offscreen or backgrounded and honor reduced motion. Recheck the current request and frame on resume.
Swift, SwiftUI and Apple platforms
Use a persistent native view with two AVPlayer instances and two
AVPlayerLayer instances. SwiftUI should retain that view through a
UIViewRepresentable or NSViewRepresentable, instead of recreating it per URL.
Install the new item on the inactive player only. Observe the candidate layer's
isReadyForDisplay
and, for the stricter handoff used by Life on iOS, attach an
AVPlayerItemVideoOutput. Verify that a pixel buffer can be copied for the current
item's presentation time before revealing the layer. Reset old readiness when
replacing an item. AVPlayerItem.status == .readyToPlay alone is insufficient.
Perform both layer-opacity changes on the main thread in one CATransaction
with setDisableActions(true). Disable implicit animations for layer geometry
too. Leave the outgoing layer attached and displaying its last frame until this
commit. If no candidate frame arrives before the preparation deadline, keep the
outgoing layer and report the failure. Do not force the swap on timeout.
Observe end/failure notifications for the specific current item and request. Ignore outgoing-item completion while a successor prepares. Release old items, outputs and observations after the swap with slot-version checks. Muted playback and audio-session policy should preserve other app audio; handle interruption and background/resume without clearing the last visible frame.
Android
Use the same visible/requested-slot ownership with a native decoder and render surface. Masko Life uses two decoder slots and a persistent GPU compositor. It switches the selected texture only after the candidate has produced a frame, then reports readiness after presentation. A player-prepared event alone is not a displayed frame. Keep the old texture until the replacement is ready and cancel obsolete requests before releasing their surfaces.
Use a transparency format that your renderer actually supports. Life's Android path decodes packed color/alpha media with a shader; it is not ordinary transparent MP4 playback. Do not copy that path unless the delivered asset has the matching layout and the shader implements it.
Graph timing and media preparation
Feed app events to the graph controller, then pass the chosen edge to the player. Preload likely successor media while the current clip plays, within a bounded cache and the two-player budget. Preserve the authored loop exit and interruption rules. Do not cut every action short when a newer app event arrives.
Use player completion and verified presentation to drive the playback lifecycle.
Do not use setTimeout(duration) as the authority for completion: buffering,
seeking, speed changes and background suspension alter wall-clock timing. If an
outgoing clip ends before B is ready, hold its last frame. Looping an idle clip
can continue only where the graph permits it. A seamless repeated loop also
requires matching first and last poses; a decoder handoff cannot repair the art.
Private signed delivery must be fetched through your authorized backend. Refresh expiring URLs before staging the next load, preserving the current visible clip. Do not put authoring credentials in a browser or mobile app.
Verify the result
Record and inspect idle → action → idle, several successive actions, and repeated loops. Test cold media, warm cache, slow network, failed decoding, expired URLs, rapid replacement requests, seeking and background/resume. Check transparency over white, black and colored backgrounds, including resized/high-density views. There should be no empty frames, black flashes, double images or stale actions.
Record request ID, slot version, frame-ready time, handoff time, end and failure. A preparation failure must leave the prior picture visible. Report which devices, browsers and paths were actually tested. Following this guide alone does not prove visual parity with Desktop or Life.