Skip to content

Vertical feed

OGVerticalFeedView is a swipeable, full-screen vertical video feed built on the OGPlayer engine — a separate surface from the standard player, portrait-only by design (it exposes no fullscreen or rotation API; pin your screen to portrait). Under the hood an internal pool of at most three engine instances keeps the current item playing while its neighbours sit prepared — muted, paused, on capped standby buffers — so swipes start instantly and memory stays predictable.

The feed ships on every platform with the same API names and semantics: OGVerticalFeedView on Android and iOS, <og-vertical-feed> on the web.

OGVerticalFeedView(
items = listOf(
OGVerticalFeedItem.Builder(
OGMediaItem.Builder("https://cdn.example.com/clips/one.mp4").build(),
)
.setPosterUrl("https://cdn.example.com/clips/one.jpg")
.setTitle("First clip")
.setSubtitle("@creator · What the caption says")
.build(),
// …more items
),
)

The feed ships bare by design: video, tap-to-pause, and a progress hairline. Titles, subtitles, rail buttons, badges — everything below is explicit opt-in configuration. The SDK never infers behavior from content.

Each OGVerticalFeedItem carries its media plus feed-specific presentation:

  • setPosterUrl — shown for pages outside the player window (and until first frame)
  • setTitle / setSubtitle — rendered only when the config enables them
  • setRailActions — up to six host buttons for the right rail (see below)
  • setSponsored(true) — marks an ad item (see Sponsored items)
  • setContentFit — per-item FILL (crop, default) or FIT (letterbox)
  • setTextPlacementOVERLAY (default), or the split layouts below
  • setSecondaryMedia / setSplitAudioSource — the split-video layout below

On iOS the same properties are init parameters on the OGVerticalFeedItem struct (posterUrl:, title:, subtitle:, railActions:, isSponsored:, contentFit:, textPlacement:, secondaryMedia:, splitAudioSource:), and OGVerticalFeedConfig is a plain struct — set var config = OGVerticalFeedConfig() properties directly instead of using a builder. On the web, items and config are plain objects with the same property names, assigned to the element (feed.items = […], feed.config = {…}); events arrive as DOM CustomEvents (og-activeitemchanged, og-nearend, og-itemskipped, og-itemerror, og-analytics, og-mutedfallback), and the sponsoredBadge / errorOverlay slots are properties returning DOM elements.

One component, three page layouts, chosen per item:

Vertical (default) — the video fills the page; title/subtitle overlay bottom-left when enabled.

Split screen: text + video — the page splits into a text section and a video section. Made for footage that shouldn’t be cropped to portrait: the text lives in its own band, never over the picture.

OGVerticalFeedItem.Builder(media)
.setTitle("Headline")
.setSubtitle("A longer description — up to six lines in the band.")
.setTextPlacement(TextPlacement.ABOVE_VIDEO) // or BELOW_VIDEO
.build()

Sizing and styling are feed-level config: setTextBandFraction(0.15f..0.5f) (default one third), setTextBandColor, setTextBandTopMargin, setTextFontFamily, setTitleTextSize, setSubtitleTextSize.

Split video: two sources — two videos on one page, stacked. The primary plays on top and owns scrubbing, progress and analytics; the secondary plays below, starts in the same frame as the primary (the feed synchronizes their start), and pauses/resumes with it. Exactly one half is audible:

OGVerticalFeedItem.Builder(topMedia)
.setSecondaryMedia(bottomMedia)
.setSplitAudioSource(SplitAudioSource.SECONDARY) // bottom half owns audio
.build()

Pair clips of equal duration — both halves loop on their own timelines, so matched lengths keep their restarts together. The secondary player exists only while its page is on screen and never consumes the preload budget.

The right rail is an empty placeholder the host fills — the SDK ships no built-in buttons there. A RailAction is an icon, an optional label (a count, a state word), an isActive flag and a click handler; update state by re-supplying the items list:

RailAction(
iconRes = R.drawable.ic_like,
label = "12.4K",
isActive = liked,
onClick = { toggleLike() },
)

In split layouts the rail anchors inside the video section, never over text.

  • Tap = instant pause with a center glyph (replaceable via setPlayIconRes on Android, config.playIcon on iOS, playIconSvg on the web); a spinner shows while loading — the glyph strictly means “viewer paused”.
  • onItemDoubleTapped — a plain callback; the SDK attaches no meaning. When set, single-tap pause waits ~300 ms for gesture disambiguation.
  • The progress hairline scrubs — drag anywhere on it, no thumb. Configure with setShowProgressBar, setProgressBarThickness, setProgressBarColor, setProgressBarTrackColor.
val state = rememberOGVerticalFeedState()
OGVerticalFeedView(items = items, state = state)
// anywhere in your UI:
state.isMuted = true // sticky across swipes; applies to the audible half
val index = state.currentIndex

There is no built-in mute button — muting is host UI, one line of state.

Feed advertising works the way vertical feeds work everywhere: the ad is a native feed item your server inserts into the list. Mark it setSponsored(true) and the SDK renders the default “Sponsored” chip, never loops it, and auto-advances to the next item when it ends. Customize with setShowSponsoredBadge(false) or replace the chip via the view’s sponsoredBadge { item -> … } slot. VAST/IMA ad breaks intentionally do not exist inside the feed — that model belongs to the standard player.

The SDK never fetches feed data. Listen and append:

OGVerticalFeedView(
items = items,
onNearEnd = { remaining -> viewModel.loadNextPage() },
onActiveItemChanged = { index, item -> /* … */ },
onItemSkipped = { index, reason -> /* e.g. durationExceeded */ },
)

setMaxItemDurationSeconds rejects over-long items at prepare time — they never buffer media and are skipped with an onItemSkipped callback.

The feed mirrors the standard player’s three levels of error control, feed-shaped:

OGVerticalFeedView(
items = items,
// 1. The callback — same error object as the standard player's onError,
// plus which feed item it hit.
onItemError = { index, error -> log(index, error) },
// 3. Or your whole error UI on the failed page (the feed counterpart of
// the standard player's errorOverlay slot).
errorOverlay = { error, retry ->
YourErrorPanel(code = error.code, onTryAgain = retry)
},
)
// 2. Or just your copy on the SDK's default error state:
OGVerticalFeedConfig.Builder()
.setErrorMessageProvider { error -> "Oeps — fout ${error.code}" }
.build()

Without customization, a failed page shows the SDK’s compact default — dimmed surface, message, Retry — styled with the feed’s font and text-size config. Errors also arrive in the analytics stream as the standard AnalyticsEvent.Error. A failed page never blocks the feed: swiping past it always works, and that is the feed-native recovery gesture.

Feed behaviour reports through the same AnalyticsEvent family as playback, with a VerticalFeed prefix so mappers can separate the two:

EventWhen
VerticalFeedImpression(index, url)an item became the visible page
VerticalFeedItemLooped(index, loopCount)the active item restarted
VerticalFeedItemWatched(index, url, watchTimeMs, loopCount)the item left the screen — true watch-time, loops included
OGVerticalFeedView(items = items, onAnalyticsEvent = { event -> analytics.track(event) })

The pool holds at most preloadAhead + keepBehind + 1 (≤ 3) engine instances. Preloaded neighbours buffer on a capped standby profile (8 MB / 10 s) and switch to full buffering only when promoted. Items outside the window are recycled without tearing down decoders; everything is released when the feed leaves the screen. On low-end targets set setPreloadAhead(0) explicitly. Encode feed content for feeds — 540p at around 1 Mbps is the norm for full-screen portrait clips.

DRM-protected items (untested in the pool in this release), VAST/IMA breaks, PiP and casting, landscape orientation of any kind. The standard player covers all of these.