A Skip intro button is a small thing to see and a surprisingly fussy thing to build. The button is easy. The hard questions are where the times come from, and what to do when two places have an answer.
This post is about those choices in Yonto Player, a player for your own library on Android TV, phones and tablets. Server markers came first; the community lookup arrived in 5.4.0.
Two sources, in a fixed order
The player knows about an intro, a recap or the credits only if something told it. There are two places that can:
- Your server's own markers. Plex, Jellyfin and Emby can each mark these stretches themselves, after their own scan. A source plugin reads them and hands the player a list of segments with the stream.
- A community database, for the gaps. Where the source marked no intro or no credits, the app asks TheIntroDB, a community database of skip times, for that video.
The order is the whole design. The server is the authority on its own files, so whatever it marked is used as it is and nothing replaces it.
Here is a simplified sketch of the precedence, with generic names (not the real code):
// Simplified sketch of the precedence, not the real code.
fun segmentsToUse(fromServer: List<Segment>, fromDatabase: List<Segment>): List<Segment> {
val kindsServerMarked = fromServer.map { it.type }.toSet()
// The database only adds kinds the server left out.
return fromServer + fromDatabase.filter { it.type !in kindsServerMarked }
}
It works per kind. A server that marked the intro but not the credits keeps its intro, and the credits come from the database if it has them.
What a segment is
Both sources end up in the same small model, so the player does not need to know where a segment came from:
// Simplified sketch of the segment model.
enum class SegmentType { INTRO, RECAP, CREDITS, PREVIEW, COMMERCIAL }
data class Segment(
val type: SegmentType,
val startMs: Long,
val endMs: Long? = null, // null means "to the end of the video"
)
A null end is a segment that runs to the end of the file, such as final credits. Only intro, recap and credits get a button; the other two kinds are carried but not offered.
Plugins send seconds over the plugin contract, which is public in the plugin reference, and the app keeps milliseconds. Each plugin converts its server's own unit, and a bad item is dropped on its own: an unknown type, a negative start, or an end that is not after the start. The list is capped at 32 items.
Asked by id, not by name
A lookup is built from one public id of the title (the TheTVDB id if there is one, otherwise TMDB, otherwise IMDb), plus the season and episode number for an episode, plus the video's length. The length is there because it tells a theatrical cut from an extended one. The request goes straight from the device to the database, with no server of ours in between, and carries the app's name and version as its user agent. The database sees the request, as with any web request. It is asked by id and not by title or file name, so some things are not looked up:
- A title with no id. A file the library could not match, for example, gets no lookup. The alternative would be to search by name, so the player shows no button instead.
- An episode whose numbers are not known. Asking for a series without a season and episode would be a guess.
- A live stream. It has no end, so there is nothing to look up.
- A source that marked both the intro and the credits. There is nothing to fill, so there is no request. A missing recap alone is not worth one either.
There is also a switch, Look up skip times online, on the Skip intro and credits page in Settings. The viewer can turn it off, and then the player uses only what the source marks.
The lookup does not make playback wait
The ask happens only once playback has started. The player waits until the engine reports the video's length, or for five seconds without one, and then asks once. Before that, nothing is on the network path of pressing Play.
Asking from the title page would be earlier, but the length that picks the right release is only known once the video opens, and a title that is opened and never played would spend a request for nothing.
The cost of asking late is small. On a video whose intro begins straight away, the button can appear a moment after the start. A segment that arrives late still gets its button while it plays.
The answers are also kept on the device, so a binge does not ask twice for the same thing:
- an answer with segments is kept for a week,
- an answer of "nothing found" for a day,
- a failure not at all, so the next play tries again.
The key includes the length rounded to the minute, so a different release is its own answer, and the store is bounded at 1,000 answers. Clear cache in Settings empties it. If the database asks for a wait, the app stops asking until the wait is over, and a play does not fail because of the lookup.
Fitting an answer to the file
An answer is used only when it fits this file. A segment of no length is how the database says "none", so it is not used. Credits are taken only from the second half of the video, since skipping credits can lead to the next episode. Segments under two seconds are ignored.
How the button decides its label
The player looks at the position four times a second. A small tracker decides, on each look, whether there is a segment to offer, and offers each segment once it begins.
The label comes from the segment, with one twist. An intro says Skip intro, a recap says Skip recap, and credits say Skip credits, unless the episode has a next one, in which case the button says Next episode and plays it.
// Simplified sketch of the label.
fun label(segment: Segment, hasNextEpisode: Boolean): Text = when (segment.type) {
INTRO -> Text.SkipIntro
RECAP -> Text.SkipRecap
CREDITS -> if (hasNextEpisode) Text.NextEpisode else Text.SkipCredits
else -> error("never offered")
}
The viewer chooses per kind, between Ask (the default), Skip automatically and Never skip. Ask shows the button for 8 seconds. Skip automatically shows a 5-second countdown that Back cancels, so nothing jumps in the middle of a line without warning. A segment shorter than two seconds is treated as a marker's slip and gets no button, and the time only counts down while the video is playing, so a pause holds it.
Once you put the button away, or an Ask's 8 seconds run out, moving around inside the segment does not bring it back. Going back to before the segment, or back into it after you got past it, offers it again, so a skip you did not want is one press from undone.
Automatic skipping is opt-in, so Ask is what a new install does.
What it deliberately does not do
It does not find intros by itself. It does not listen to the audio, fingerprint a season or scan the picture for black frames. It reads what your server marked and what the community database knows.
That means a new or rare title may have no button at all, and I think that is the right trade for now. In my view a missing button is better than one in the wrong place, so guessing from the clock, such as "skip the first 85 seconds", is out too.
There is one exception to the no-guessing rule, and it is not a skip. The Next episode offer near the end of an episode falls back on the last 45 seconds (or the last tenth of a short episode) when there is no credits segment. A real credits segment replaces that guess when there is one.
What you can take from it
If you build something like this: let the authority on the files win, per kind. Ask a third party with identifiers. Start the lookup after playback. Cache answers and non-answers, but not failures. And write down what the feature will not do.
Read more
- Skip intro and credits, the public description of the feature
- Sources and what each one does
-
Plugin reference, where a stream's
segmentsare defined
Top comments (0)