Skip to navigation

Sideload external subtitles

Learn how to register, select, and render external subtitle files that are not in the HLS manifest in the FastPix iOS Player.

The FastPix iOS Player SDK can play a subtitle file that is not in the manifest: a .vtt or .srt file side-loaded from your own storage, alongside an asset you cannot re-package. The SDK supports two formats, WebVTT and SubRip. SubRip files are converted to WebVTT on the device and reported as srt_converted, so a timing problem can be traced back to the conversion step.

External tracks appear in getSubtitleTracks() alongside in-manifest tracks and are selected with the same setSubtitleTrack(trackId:). External cues always arrive through the onSubtitleCueChange callback, so your app renders them itself. For the shared subtitle workflow, see Switch subtitle tracks.

Register an external subtitle

Register the track after prepare(playbackID:playbackOptions:). The subtitle manager is created during player setup, so registering before it exists fails with .playerNotPrepared.

Registration is synchronous and performs no I/O. It answers only what can be decided from the descriptor you pass. Questions that need the file bytes, such as whether the file exists or contains readable cues, are answered later through the delegate when the track is first selected.

// External cues always arrive through onSubtitleCueChange, so render them yourself.
playerViewController.subtitleTrackDelegate = self
let descriptor = FastPixExternalSubtitleTrack(
sourceID: "en-forced-v1",
url: "https://cdn.example.com/subs/en.srt",
language: .declared("en"),
label: "English (Forced)",
format: nil, // inferred from the path extension
isDefault: false,
isForced: true
)
switch playerViewController.registerExternalSubtitle(descriptor) {
case .success(let track):
// `track.id` is what you pass to setSubtitleTrack(trackId:)
playerViewController.setSubtitleTrack(trackId: track.id)
case .failure(let error):
print("[Subtitles] registration refused:", error.explanation)
}

Unregister and inspect tracks

Registering re-publishes onSubtitlesLoaded(tracks:), so a track registered after the manifest has already loaded still appears in your menu. Unregistering the track that is currently rendering stops its cue stream and fires onSubtitleChange(track: nil).

let removed = playerViewController.unregisterExternalSubtitle(sourceID: "en-forced-v1")
for track in playerViewController.registeredExternalSubtitles {
print(track.sourceID, track.label, track.format as Any)
}

Declare the format explicitly

The SDK infers the format from the URL’s path extension only. The query string is not read as a statement about the file’s format. A signed URL such as …/en.vtt?token=ey… is inferred correctly, but …/subtitles?file=en.srt is not, so you must declare the format.

let descriptor = FastPixExternalSubtitleTrack(
sourceID: "es-1",
url: "https://cdn.example.com/subtitles?file=es",
language: .declared("es"),
label: "Spanish",
format: .subRip // required: no usable extension
)

Handle external subtitle events

Conform to FastPixSubtitleTrackDelegate to receive load reports, load failures, and rendering limitations:

extension VideoPlayerViewController: FastPixSubtitleTrackDelegate {
// The file was fetched and parsed.
func onExternalSubtitleLoaded(report: FastPixExternalSubtitleLoadReport) {
print("[Subtitles]", report.sourceID,
"ingest:", report.ingestFormat.rawValue, // "vtt" or "srt_converted"
"cues:", report.cueCount,
"firstCue:", report.firstCueStart as Any)
}
// The file could not be turned into cues.
func onExternalSubtitleLoadFailed(sourceID: String,
failure: FastPixExternalSubtitleLoadFailure) {
print("[Subtitles]", sourceID, "failed:", failure.explanation)
}
// Fired when host rendering becomes active, with what is now true that was not before.
func onSubtitleRenderingLimitations(_ limitations: [FastPixExternalSubtitleLimitation]) {
limitations.forEach { print("[Subtitles] limitation:", $0.explanation) }
}
}

The same list is queryable at any time, which makes a “subtitles vanished on AirPlay” report answerable in one line:

print(playerViewController.subtitleRenderingLimitations)

Bound the fetching window

Control how far ahead and behind the playhead the SDK fetches and retains cues:

playerViewController.subtitleWindowPolicy = FastPixSubtitleWindowPolicy(
lookAhead: 60,
lookBehind: 30,
maxConcurrentFetches: 3,
maxRetainedCues: 5_000
)
OptionTypeDescriptionDefault
lookAheadTimeIntervalHow far ahead of the playhead to fetch60
lookBehindTimeIntervalHow far behind to keep fetching, so a short seek back does not blank the cues30
maxConcurrentFetchesIntSimultaneous segment fetches3
maxRetainedCuesIntCues held in memory; those furthest from the playhead are evicted first5000

FastPixExternalSubtitleTrack model

PropertyTypeDescription
sourceIDStringYour identifier. Becomes the identity discriminator and must be stable across registrations
urlStringhttp, https, or file
languageFastPixSubtitleLanguage.declared("en"), or .undetermined when you do not know. Never guessed from label
labelStringThe only text you can put in a subtitle menu; must not be empty
formatFastPixSubtitleFileFormat?nil to infer from the path extension
isDefaultBoolAdvisory only. The SDK never auto-selects an external track on the strength of this
isForcedBoolForced-narrative flag, for your own menu presentation
identityFastPixSubtitleTrackIdentityDerived. stringValue is external:<sourceID>:<languageTag> and is the SubtitleTrack.id

FastPixSubtitleFileFormat and FastPixSubtitleIngestFormat

CaseMeaning
.webVTTRecognized from .vtt or .webvtt. Parsed as delivered
.subRipRecognized from .srt. Converted to WebVTT on the device, then parsed
.webVTT (ingest, "vtt")Cues came straight from a WebVTT file
.subRipConverted (ingest, "srt_converted")The SDK rewrote the file to produce these cues

FastPixExternalSubtitleRegistrationError

Decidable from the descriptor alone, reported synchronously at the call site. Every case has an explanation.

CaseMeaning
.emptySourceIDAn empty ID makes every external track collide with every other one
.duplicateSourceID(String)Already registered; unregister the first one to replace it
.emptyLabelAn unnamed row is not something a viewer can choose between
.malformedURL(String)Not a parseable URL, or a relative reference with no scheme
.unsupportedScheme(String)Only http, https, and file are accepted
.indeterminateFormat(String)The path extension says nothing. Pass format:; the SDK will not read the file to find out
.playerNotPreparedCalled before prepare(playbackID:playbackOptions:)

FastPixExternalSubtitleLoadFailure

Needs the file bytes, so it is reported asynchronously when the track is first selected.

CaseMeaning
.unreachable(FastPixSubtitleFetchError)The file could not be retrieved; carries the transport’s own account
.noCuesThe file arrived and parsed to nothing. Check that the declared format matches the file

FastPixExternalSubtitleLoadReport

PropertyTypeDescription
sourceIDStringThe track that loaded
ingestFormatFastPixSubtitleIngestFormatvtt, or srt_converted when the SDK rewrote the file
cueCountIntCues parsed
firstCueStartTimeInterval?The cheapest sanity check on a timing complaint. A first cue at 0 for dialogue starting at 12 s is a file for the wrong asset

FastPixExternalSubtitleLimitation

CaseMeaning
.notRenderedInPictureInPicturePicture in Picture presents the player’s video surface; a cue drawn into a sibling view is not part of it
.notRenderedOnExternalDisplayThe same for AirPlay and a wired display: the surface travels, your view hierarchy does not
.systemCaptionAppearanceNotAppliedSettings > Accessibility > Subtitles & Captioning styles AVFoundation’s rendering, not your label
.notRenderedByAVFoundationAVFoundation renders legible tracks that exist in the asset. A side-loaded file is not in the asset, so .avFoundation is not an available renderer for it

Two convenience lists are published: FastPixExternalSubtitleLimitation.hostRendering (the first three) and .externalTrack (all four).

Note:

  • registerExternalSubtitle(_:) performs no I/O and returns synchronously. It does not stat the file, including for a file:// URL, which can point at an iCloud Drive path where fileExists blocks on a download. Existence at registration would also say nothing about existence at selection.
  • An external track has exactly one possible renderer. The SDK selects .host for it regardless of subtitleRenderer, because AVFoundation renders legible tracks that exist in the asset and a side-loaded file does not. Implement onSubtitleCueChange(information:) to see anything.
  • For in-manifest tracks the renderer is yours to choose. subtitleRenderer defaults to .avFoundation, which draws inside the player’s own surface and does not emit the cue stream; set .host to receive cues instead. Do not do both, or a host ends up rendering every subtitle twice.
  • sourceID is yours to choose precisely so it can be stable across app launches. An SDK-generated ID would change on every relaunch, and a host persisting the viewer’s choice would never find the track again.
  • isDefault does not auto-select. A side-loaded default would override the stream’s own DEFAULT=YES rendition without you having asked for that.
  • The composite ID (external:<sourceID>:<language>) exists so that two tracks in the same language are addressable separately. Legacy language-only IDs passed to setSubtitleTrack(trackId:) still resolve.

Frequently asked questions

What subtitle file formats can I sideload into the FastPix iOS Player?

You can sideload WebVTT (.vtt) and SubRip (.srt) files. SubRip files are converted to WebVTT on the device and reported with the ingest format srt_converted, so a timing problem can be traced back to the conversion step. The SDK infers the format from the URL’s path extension, or you can set format: explicitly when the URL has no usable extension.

Why are my external subtitles not showing up in the FastPix iOS Player?

An external track has exactly one possible renderer, so the SDK renders it through the host, not AVFoundation. You must set the subtitleTrackDelegate and implement onSubtitleCueChange(information:) to draw the cues yourself. Also register the track after prepare(playbackID:playbackOptions:); registering earlier fails with .playerNotPrepared.

Can I sideload a subtitle file stored locally on the device?

Yes. The url on FastPixExternalSubtitleTrack accepts http, https, and file schemes. Registration performs no I/O and does not check that the file exists, including for a file:// URL, so a missing or unreadable file is reported asynchronously through onExternalSubtitleLoadFailed(sourceID:failure:) when the track is first selected.

Why do my external subtitles disappear during AirPlay or Picture in Picture?

Host-rendered cues are drawn into a sibling view, and AirPlay, external displays, and Picture in Picture move the player’s video surface rather than your view hierarchy. The SDK reports this through onSubtitleRenderingLimitations(_:), and the current limitations are queryable at any time with subtitleRenderingLimitations.