Event Playback
Pre-requisite: Fetching event tokens requires a signed-in user;
eventM3U8needs only a configured SDK.
Event video is served as an HLS playlist (.m3u8). Two things stand between the URL on the event and a playing video: the storage bucket wants an authorization token on the URL, and a player that cannot set its own headers needs the playlist fetched with the SDK’s authentication. Both are covered here.
Generating an Event Playback URL
The video URL on an event may already carry an Authorization query parameter, but the token in it is short lived. Whether or not one is there, replace it with a freshly fetched token before playing.
Start by fetching the Event Tokens:
Factory.spaceService.getB2AuthToken(spaceId: spaceId)
.receive(on: DispatchQueue.main)
.sink { result in
guard case let .success(tokens) = result else { return }
self.tokens = tokens // [B2AuthTokenModel]
}
.store(in: &cancellables)
Each B2AuthTokenModel carries an authToken along with the bucketName and deviceId it is valid for. Match on both — a space holds several buckets and several cameras, and the token for one camera’s bucket will not authorize another’s:
func authorizedURL(for event: EventModel, tokens: [B2AuthTokenModel]) -> URL? {
guard let videoUrl = event.videoUrl,
var components = URLComponents(string: videoUrl),
let token = tokens.first(where: {
$0.bucketName == event.bucketName && $0.deviceId == event.deviceId
})
else { return nil }
var queryItems = components.queryItems ?? []
queryItems.removeAll { $0.name == "Authorization" } // drop the stale one
queryItems.append(URLQueryItem(name: "Authorization", value: token.authToken))
components.queryItems = queryItems
return components.url
}
So an event whose video URL is https://www.example.com/video.m3u8 and whose bucketName is 180-days is played from https://www.example.com/video.m3u8?Authorization=123123123123123, using the token whose bucketName is 180-days and whose deviceId matches the event’s camera.
One fetch covers every event in the space, so fetch the tokens once and hold them rather than calling getB2AuthToken per event — the reference app caches them for a day and refetches when a lookup misses. Refetch and rebuild the URL if playback starts failing with an authorization error; the token has expired, not the video.
Build the URL at the moment you play, not when you build the list. A URL authorized minutes ago may already be stale by the time the user taps it.
Choosing a player
Any player that can decode H.265 (HEVC) inside MPEG-TS segments will play these events, and it also has to let you attach the authorization — either as a header on every segment request or by playing a playlist whose segment URLs already carry the token.
That second requirement rules out less than the first does. AVPlayer supports HEVC in HLS only when the segments are fragmented MP4; it will not decode HEVC carried in MPEG-TS, which is what these playlists contain. An FFmpeg-backed player handles it, which is why the reference app uses IJKPlayer — but it is an example, not a requirement. Any player built on FFmpeg, or any other one that decodes HEVC in TS, works the same way.
With IJKPlayer, the authorization goes in as a format option and the TS protocols have to be on the whitelist:
let options = IJKFFOptions.byDefault()
options?.setFormatOptionValue("Authorization: \(token.authToken)\r\n", forKey: "headers")
options?.setFormatOptionValue("file,http,https,tcp,tls,ts", forKey: "protocol_whitelist")
options?.setPlayerOptionIntValue(1, forKey: "videotoolbox") // hardware decoding
player = IJKFFMoviePlayerController(contentURL: url, with: options)
player?.scalingMode = .aspectFit
player?.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
player?.shouldAutoplay = false
containerView.addSubview(player!.view)
player?.prepareToPlay()
Shut the player down when the view goes away, or the decoder and its socket stay alive:
player?.pause()
player?.shutdown()
player?.view.removeFromSuperview()
player = nil
videotoolbox turns on hardware decoding, which matters for HEVC — without it a multi sensor or high resolution event will drop frames on older devices. The protocol whitelist is not optional: FFmpeg refuses to follow a playlist into protocols that are not listed, and the segments are ts over https.
Get the Event Playlist
To fetch the HLS playlist of a recorded event with the SDK authentication applied, The following method can be used:
url(required): The playlist URL of the event.
Factory.systemService.eventM3U8(url: String) -> IVPublisher<Data>
This call only needs the SDK to be configured — it does not require a signed-in user.
Use it when your player cannot attach a header to every segment request. Fetch the playlist, append the token to each .ts line, write the result to a temporary file, and point the player at that file instead of the remote URL:
Factory.systemService.eventM3U8(url: authorizedURL.absoluteString)
.sink { result in
guard case let .success(data) = result,
let playlist = String(data: data, encoding: .utf8) else { return }
var base = URLComponents(url: authorizedURL.deletingLastPathComponent(),
resolvingAgainstBaseURL: false)
base?.query = nil
let baseUrl = base?.url?.absoluteString ?? ""
var lines = playlist.components(separatedBy: "\n").map { line -> String in
line.hasSuffix(".ts")
? baseUrl + line + "?Authorization=" + token.authToken
: line
}
if !lines.contains(where: { $0.contains("#EXT-X-ENDLIST") }) {
lines.append("#EXT-X-ENDLIST")
}
let localUrl = FileManager.default.temporaryDirectory
.appendingPathComponent("\(event.id).m3u8")
try? lines.joined(separator: "\n").data(using: .utf8)?.write(to: localUrl)
self.play(localUrl) // the player never sees a remote playlist
}
.store(in: &cancellables)
Two details make the difference between this working and not. The segment lines in the playlist are relative, so each has to be prefixed with the playlist’s own directory before the token is appended. And a playlist for an event that is still being written has no #EXT-X-ENDLIST tag; without it the player treats the file as a live stream and will not start from the beginning, so append the tag for a finished event.
Because the local playlist points at absolute segment URLs, keep file in the player’s protocol whitelist alongside the network ones — the playlist is read from disk while the segments come over https.
Footage still on the camera’s card is streamed rather than downloaded — see SD Card Playback.