Complete guide to integrating Pubfuse's live streaming platform into your applications
The Pubfuse SDK allows you to integrate live streaming capabilities into your applications. With our API-first approach, you can build custom streaming experiences while leveraging Pubfuse's robust infrastructure.
Each SDK Client operates in isolation with their own users, sessions, and data. Perfect for white-label solutions.
Industry-standard API key authentication with optional HMAC signature verification for enhanced security.
First, you need to register your application to get API credentials.
/api/admin/sdk-clients
{
"name": "My Streaming App",
"website": "https://myapp.com",
"description": "My awesome streaming application",
"contactName": "John Developer",
"contactTitle": "Lead Developer",
"contactEmail": "[email protected]",
"contactPhone": "+1234567890",
"expectedApps": "1-5",
"useCase": "Live streaming for events",
"expectedUsers": "100-1000",
"agreeTerms": true,
"agreeMarketing": false,
"agreeDataProcessing": true
}
{
"success": true,
"apiKey": "pk_5951C5196A6C47EDA12D41B9A050AC5C",
"secretKey": "sk_8510CDE5A7ED4E749D15E1008FBD7B7E",
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"message": "SDK Client registered successfully"
}
GET /api/users/profile/full – Full profile with follower/following countsPUT /api/users/profile – Update profile fieldsDELETE /api/users/profile – Delete current userPOST /api/users/change-password – Change passwordDELETE /api/users/follow/:id – Unfollow user by idDELETE /api/contacts/:id/follow – Unfollow by contact idPUT /api/contacts/:id – Update a stored contact// Update profile
struct UpdateProfileRequest: Codable {
let username: String?
let firstName: String?
let lastName: String?
let avatarUrl: String?
let email: String?
let phoneNumber: String?
}
func updateProfile(token: String, body: UpdateProfileRequest) async throws {
var req = URLRequest(url: URL(string: "${baseUrl}/api/users/profile")!)
req.httpMethod = "PUT"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
req.httpBody = try JSONEncoder().encode(body)
let _ = try await URLSession.shared.data(for: req)
}
// Delete profile
func deleteProfile(token: String) async throws {
var req = URLRequest(url: URL(string: "${baseUrl}/api/users/profile")!)
req.httpMethod = "DELETE"
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let _ = try await URLSession.shared.data(for: req)
}
// Change password
struct ChangePasswordRequest: Codable { let currentPassword: String; let newPassword: String }
func changePassword(token: String, body: ChangePasswordRequest) async throws {
var req = URLRequest(url: URL(string: "${baseUrl}/api/users/change-password")!)
req.httpMethod = "POST"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
req.httpBody = try JSONEncoder().encode(body)
let _ = try await URLSession.shared.data(for: req)
}
// Unfollow a user by user id
func unfollowUser(token: String, userId: String) async throws {
var req = URLRequest(url: URL(string: "${baseUrl}/api/users/follow/\(userId)")!)
req.httpMethod = "DELETE"
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let _ = try await URLSession.shared.data(for: req)
}
// Update a contact
struct ContactUpdate: Codable { let phoneNumber: String?; let displayName: String?; let firstName: String?; let lastName: String?; let email: String? }
func updateContact(token: String, contactId: String, body: ContactUpdate) async throws {
var req = URLRequest(url: URL(string: "${baseUrl}/api/contacts/\(contactId)")!)
req.httpMethod = "PUT"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
req.httpBody = try JSONEncoder().encode(body)
let _ = try await URLSession.shared.data(for: req)
}
# Update profile
curl -X PUT "$BASE/api/users/profile" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"username":"newname","firstName":"New","lastName":"Name","email":"[email protected]","phoneNumber":"+1234567890"}'
# Delete profile
curl -X DELETE "$BASE/api/users/profile" -H "Authorization: Bearer $TOKEN"
# Change password
curl -X POST "$BASE/api/users/change-password" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"currentPassword":"oldpass","newPassword":"newpass123"}'
# Unfollow by user id
curl -X DELETE "$BASE/api/users/follow/USER_ID" -H "Authorization: Bearer $TOKEN"
# Update a contact
curl -X PUT "$BASE/api/contacts/CONTACT_ID" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"displayName":"Jane S."}'
All API requests require authentication using your API key. Include it in the X-API-Key header.
curl -X GET https://api.pubfuse.com/api/v1/sessions \
-H "X-API-Key: pk_5951C5196A6C47EDA12D41B9A050AC5C" \
-H "Content-Type: application/json"
For enhanced security, use HMAC signature authentication with your secret key.
const crypto = require('crypto');
function generateSignature(method, path, body, timestamp, secretKey) {
const payload = `${method}${path}${body}${timestamp}`;
return crypto.createHmac('sha256', secretKey)
.update(payload)
.digest('hex');
}
const method = 'POST';
const path = '/api/v1/sessions';
const body = JSON.stringify({ title: 'My Stream' });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = generateSignature(method, path, body, timestamp, secretKey);
fetch('https://api.pubfuse.com/api/v1/sessions', {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'X-Signature': signature,
'X-Timestamp': timestamp,
'Content-Type': 'application/json'
},
body: body
});
/api/v1/sessions
Create a new streaming session
/api/v1/sessions
List all sessions for your SDK client
/api/v1/sessions/{id}
Update session status and metadata
/api/users/signup
Register a new user
✅ WORKING/api/users/login
Authenticate a user
✅ WORKING/api/v1/users
List users for your SDK client
/ws/streams/{id}/chat
Real-time chat connection
/api/v1/sessions/{id}/reactions
Send reactions to a stream
Pubfuse now supports LiveKit SFU streaming for better scalability and performance. LiveKit supports 100+ concurrent participants with built-in recording capabilities.
See our LiveKit Setup Guide for complete integration instructions.
Quick sanity-check tool: /livekit-test (connect → publish → remote subscribe)
// 1. Get streaming provider configuration
const providers = await fetch('/api/streaming/providers').then(r => r.json());
const activeProvider = providers.find(p => p.isConfigured);
// 2. Create streaming session
const session = await fetch('/api/streaming/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'My Live Stream',
visibility: 'public',
maxParticipants: 1000
})
}).then(r => r.json());
// 3. Generate LiveKit JWT access token ✅ **UPDATED**
const token = await fetch(`/api/streaming/sessions/${session.id}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your_pubfuse_api_key' // ✅ Required
},
body: JSON.stringify({
userId: 'user-123',
role: 'publisher'
})
}).then(r => r.json());
// 4. Connect to LiveKit (if provider is LiveKit)
if (activeProvider.provider.rawValue === 'livekit') {
// Load LiveKit SDK
const script = document.createElement('script');
script.src = 'https://unpkg.com/livekit-client@latest/dist/livekit-client.umd.js';
document.head.appendChild(script);
// Connect to room
const room = new LiveKit.Room();
await room.connect(token.serverUrl, token.token);
}
import Foundation
import LiveKit
class PubfuseStreamingSDK {
private var room: Room?
func connect(to streamId: String) async throws {
// Get LiveKit access token
let tokenResponse = try await getLiveKitToken(streamId: streamId)
// Create LiveKit room
let room = Room()
room.delegate = self
// Connect to LiveKit room
try await room.connect(
url: tokenResponse.serverUrl,
token: tokenResponse.token,
connectOptions: ConnectOptions(
autoManageVideo: true,
autoManageAudio: true
)
)
self.room = room
}
private func getLiveKitToken(streamId: String) async throws -> LiveKitTokenResponse {
guard let url = URL(string: "\(baseURL)/api/streaming/sessions/\(streamId)/token") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("your_pubfuse_api_key", forHTTPHeaderField: "X-API-Key") // ✅ Required
let tokenRequest = LiveKitTokenRequest(
userId: UUID().uuidString,
role: "subscriber"
)
request.httpBody = try JSONEncoder().encode(tokenRequest)
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(LiveKitTokenResponse.self, from: data)
}
}
extension PubfuseStreamingSDK: RoomDelegate {
func room(_ room: Room, didConnect isReconnect: Bool) {
print("✅ Connected to LiveKit room")
}
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTo publication: RemoteTrackPublication) {
if let videoTrack = publication.track as? VideoTrack {
videoTrack.addRenderer(videoView)
}
}
}
GET /api/streaming/providers - Get available streaming providersPOST /api/streaming/sessions - Create streaming sessionPOST /api/streaming/sessions/{id}/token - Generate LiveKit JWT access tokenGET /api/streaming/ice-config - Get ICE server configurationGET /api/livekit/health - LiveKit server health checkTokens are LiveKit-compatible and include:
nbf, iat, exp as integersroomJoin, room, canSubscribe, optional publish/data grantsX-API-KeyServer returns token prefixed with livekit_; pass it unchanged to the SDK.
Short IDs (e.g., r5) are accepted and resolved to UUIDs.
Pubfuse now supports co-host functionality, allowing viewers to join live streams as additional broadcasters. Create engaging multi-host sessions with grid-based video layouts and real-time participant management.
Perfect for interviews, panel discussions, interactive shows, and collaborative streaming experiences.
import Foundation
import LiveKit
import UIKit
class PubfuseCoHostSDK {
private var room: Room?
private let clientId = UUID().uuidString
private var streamId: String?
private let baseURL: String
private var isCoHost = false
init(baseURL: String = "https://www.pubfuse.com") {
self.baseURL = baseURL
}
/// Request to join as co-host
func requestCoHostAccess(streamId: String, userName: String) async throws -> CoHostResponse {
guard let url = URL(string: "\(baseURL)/api/streaming/sessions/\(streamId)/join-cohost") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let joinRequest = JoinCoHostRequest(
userName: userName,
permissions: CoHostPermissions.default
)
request.httpBody = try JSONEncoder().encode(joinRequest)
let (data, _) = try await URLSession.shared.data(for: request)
let response = try JSONDecoder().decode(CoHostResponse.self, from: data)
if response.success {
self.isCoHost = true
}
return response
}
/// Join as co-host with LiveKit
private func connectAsCoHost(streamId: String, userName: String) async throws {
// Get LiveKit token with publisher permissions
let tokenResponse = try await getCoHostToken(streamId: streamId, userName: userName)
// Create LiveKit room
let room = Room()
room.delegate = self
// Connect with publisher permissions
try await room.connect(
url: tokenResponse.serverUrl,
token: tokenResponse.token,
connectOptions: ConnectOptions(
autoManageVideo: true,
autoManageAudio: true,
publishDefaults: PublishDefaults(
video: true,
audio: true,
videoCodec: .h264,
audioCodec: .opus
)
)
)
self.room = room
self.isCoHost = true
}
}
class CoHostGridViewController: UIViewController {
private let streamingSDK: PubfuseCoHostSDK
private var coHostViews: [String: UIView] = [:]
private var gridLayout: GridLayoutConfig
private var streamId: String
private lazy var gridContainer: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 8
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupGridLayout()
setupCoHostButton()
}
private func setupGridLayout() {
view.addSubview(gridContainer)
NSLayoutConstraint.activate([
gridContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
gridContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
gridContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100),
gridContainer.heightAnchor.constraint(equalToConstant: 200)
])
// Create grid rows
for row in 0..<gridLayout.gridRows {
let rowStackView = createGridRow()
gridContainer.addArrangedSubview(rowStackView)
}
}
@objc private func joinAsCoHostTapped() {
Task {
do {
let response = try await streamingSDK.requestCoHostAccess(
streamId: streamId,
userName: "iOS User"
)
if response.success {
print("✅ Co-host access granted: \(response.message)")
updateUIForCoHostMode(true)
} else {
print("❌ Co-host access denied: \(response.message)")
showAlert(message: response.message)
}
} catch {
print("❌ Error requesting co-host access: \(error)")
showAlert(message: "Failed to request co-host access")
}
}
}
}
extension PubfuseCoHostSDK: RoomDelegate {
func room(_ room: Room, didConnect isReconnect: Bool) {
print("✅ Connected to LiveKit room as co-host")
if isCoHost {
// Start publishing video and audio
Task {
await room.localParticipant?.setCameraEnabled(true)
await room.localParticipant?.setMicrophoneEnabled(true)
}
}
}
func room(_ room: Room, participant: RemoteParticipant, didSubscribeTo publication: RemoteTrackPublication) {
print("📹 Subscribed to track: \(publication.track?.kind.rawValue ?? "unknown") from \(participant.identity)")
// Handle co-host video tracks
if let videoTrack = publication.track as? VideoTrack {
handleCoHostVideoTrack(videoTrack, participant: participant)
}
// Handle co-host audio tracks
if let audioTrack = publication.track as? AudioTrack {
handleCoHostAudioTrack(audioTrack, participant: participant)
}
}
func room(_ room: Room, participant: RemoteParticipant, didConnect isReconnect: Bool) {
print("👋 Co-host connected: \(participant.identity)")
// Check if this is a co-host (not main broadcaster)
if isCoHostParticipant(participant) {
addCoHostToGrid(participant: participant)
}
}
func room(_ room: Room, participant: RemoteParticipant, didDisconnect error: Error?) {
print("👋 Co-host disconnected: \(participant.identity)")
removeCoHostFromGrid(participantId: participant.identity)
}
private func isCoHostParticipant(_ participant: RemoteParticipant) -> Bool {
// Check participant metadata to determine if it's a co-host
guard let metadata = participant.metadata else { return false }
do {
let metadataDict = try JSONSerialization.jsonObject(with: metadata.data(using: .utf8) ?? Data()) as? [String: Any]
let role = metadataDict?["role"] as? String
return role == "co_host" || role == "publisher"
} catch {
// Fallback: check if participant name indicates co-host
return participant.name?.contains("Co-Host") == true ||
participant.name?.contains("User") == true
}
}
}
/api/streaming/sessions/{id}/join-cohost
Request to join as co-host
/api/streaming/sessions/{id}/multihost
Get multi-host session information
/api/streaming/sessions/{id}/cohosts/{coHostId}
Update co-host permissions
/api/streaming/sessions/{id}/cohosts/{coHostId}
Remove co-host from session
/api/streaming/sessions/{id}/metrics
Get multi-host session metrics
struct CoHostPermissions: Codable {
let canInviteOthers: Bool
let canRemoveOthers: Bool
let canControlLayout: Bool
let canModerateChat: Bool
let canTriggerAds: Bool
let canAccessAnalytics: Bool
static let `default` = CoHostPermissions(
canInviteOthers: false,
canRemoveOthers: false,
canControlLayout: false,
canModerateChat: false,
canTriggerAds: false,
canAccessAnalytics: false
)
static let moderator = CoHostPermissions(
canInviteOthers: true,
canRemoveOthers: true,
canControlLayout: true,
canModerateChat: true,
canTriggerAds: false,
canAccessAnalytics: true
)
}
struct GridLayoutConfig: Codable {
let maxHosts: Int
let gridColumns: Int
let gridRows: Int
let aspectRatio: String
let showNames: Bool
let showControls: Bool
static let `default` = GridLayoutConfig(
maxHosts: 4,
gridColumns: 2,
gridRows: 2,
aspectRatio: "16:9",
showNames: true,
showControls: true
)
}
struct CoHostResponse: Codable {
let success: Bool
let message: String
let coHost: CoHost?
let inviteToken: String?
}
import UIKit
import LiveKit
class LiveStreamViewController: UIViewController {
private let streamingSDK = PubfuseCoHostSDK()
private var coHostGrid: CoHostGridView?
private var streamId: String = ""
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = .black
// Setup co-host grid
coHostGrid = CoHostGridView()
coHostGrid?.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(coHostGrid!)
NSLayoutConstraint.activate([
coHostGrid!.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
coHostGrid!.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
coHostGrid!.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -100),
coHostGrid!.heightAnchor.constraint(equalToConstant: 200)
])
// Setup join co-host button
let joinButton = UIButton(type: .system)
joinButton.setTitle("Join as Co-Host", for: .normal)
joinButton.backgroundColor = .systemBlue
joinButton.setTitleColor(.white, for: .normal)
joinButton.layer.cornerRadius = 8
joinButton.translatesAutoresizingMaskIntoConstraints = false
joinButton.addTarget(self, action: #selector, for: .touchUpInside)
view.addSubview(joinButton)
NSLayoutConstraint.activate([
joinButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
joinButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
joinButton.widthAnchor.constraint(equalToConstant: 200),
joinButton.heightAnchor.constraint(equalToConstant: 44)
])
}
func joinStream(streamId: String) {
self.streamId = streamId
Task {
do {
// Connect as viewer first
try await streamingSDK.connect(to: streamId)
print("✅ Connected to stream as viewer")
} catch {
print("❌ Failed to connect to stream: \(error)")
}
}
}
@objc private func joinAsCoHostTapped() {
Task {
do {
let response = try await streamingSDK.requestCoHostAccess(
streamId: streamId,
userName: "iOS User"
)
if response.success {
print("✅ Co-host access granted")
updateUIForCoHostMode(true)
} else {
print("❌ Co-host access denied: \(response.message)")
showAlert(message: response.message)
}
} catch {
print("❌ Error requesting co-host access: \(error)")
showAlert(message: "Failed to request co-host access")
}
}
}
private func updateUIForCoHostMode(_ isCoHost: Bool) {
DispatchQueue.main.async {
// Update button
if let button = self.view.subviews.first(where: { $0 is UIButton }) as? UIButton {
button.setTitle(isCoHost ? "Leave Co-Host" : "Join as Co-Host", for: .normal)
button.backgroundColor = isCoHost ? .systemRed : .systemBlue
}
}
}
private func showAlert(message: String) {
DispatchQueue.main.async {
let alert = UIAlertController(title: "Co-Host", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(alert, animated: true)
}
}
}
CoHostPermissions.default for basic co-hosts, CoHostPermissions.moderator for enhanced capabilitiesGridLayoutConfig to match your app's design requirementsCreating a broadcast and starting it are separate API calls. Many iOS apps only create the broadcast but forget to set it to active status, causing it to not appear in live streams.
pendingactive (makes it discoverable)Create the broadcast entry in the database. This does NOT make it live yet.
/api/broadcasts
Creates a new broadcast session
func createBroadcast(title: String) async throws -> BroadcastResponse {
guard let url = URL(string: "\(baseURL)/api/broadcasts") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let broadcastRequest = CreateBroadcastRequest(
title: title,
visibility: "public",
tags: ["live", "broadcast"],
metadata: [
"createdBy": "iOS-App",
"clientId": UUID().uuidString,
"deviceModel": UIDevice.current.model
]
)
request.httpBody = try JSONEncoder().encode(broadcastRequest)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.broadcastCreationFailed
}
let broadcastData = try JSONDecoder().decode(BroadcastResponse.self, from: data)
// ⚠️ At this point, broadcast is created but NOT active
// Status is: "pending"
return broadcastData
}
// Response contains:
struct BroadcastResponse: Codable {
let id: String // broadcastSessionId
let streamId: String // for LiveKit room
let title: String
let status: String // "pending"
let createdAt: String
let watchUrl: String
}
Connect to LiveKit and start publishing video/audio.
func startLiveKitPublishing(streamId: String) async throws {
// Get LiveKit token with publisher permissions
let tokenResponse = try await getLiveKitToken(
streamId: streamId,
role: "publisher"
)
// Create and connect to LiveKit room
let room = Room()
room.delegate = self
try await room.connect(
url: tokenResponse.serverUrl,
token: tokenResponse.token,
connectOptions: ConnectOptions(
autoManageVideo: true,
autoManageAudio: true,
publishDefaults: PublishDefaults(
video: true,
audio: true,
videoCodec: .h264,
audioCodec: .opus
)
)
)
self.room = room
// Start camera and microphone
await room.localParticipant?.setCameraEnabled(true)
await room.localParticipant?.setMicrophoneEnabled(true)
print("✅ LiveKit publishing started")
}
This is the step most iOS apps miss! Without this, your broadcast won't appear in the live streams list.
/api/broadcasts/{id}/status
Updates broadcast status to active
func setBroadcastActive(broadcastId: String) async throws {
guard let url = URL(string: "\(baseURL)/api/broadcasts/\(broadcastId)/status") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let statusUpdate = StatusUpdateRequest(status: "active")
request.httpBody = try JSONEncoder().encode(statusUpdate)
let (_, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.statusUpdateFailed
}
print("✅ Broadcast set to ACTIVE - now discoverable!")
}
struct StatusUpdateRequest: Codable {
let status: String // "active", "completed", "error"
}
Keep the session alive by sending heartbeats every 30 seconds.
/api/sessions/{id}/heartbeat
Sends heartbeat to keep session alive
private var heartbeatTimer: Timer?
func startHeartbeat(broadcastId: String) {
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
Task {
try? await self?.sendHeartbeat(broadcastId: broadcastId)
}
}
}
func sendHeartbeat(broadcastId: String) async throws {
guard let url = URL(string: "\(baseURL)/api/sessions/\(broadcastId)/heartbeat") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let (_, _) = try await URLSession.shared.data(for: request)
print("💓 Heartbeat sent")
}
func stopHeartbeat() {
heartbeatTimer?.invalidate()
heartbeatTimer = nil
}
Report viewer count every 5 seconds for analytics.
/api/broadcasts/{id}/viewer-count
Updates current viewer count
private var viewerCountTimer: Timer?
private var currentViewerCount: Int = 0
func startViewerCountTracking(broadcastId: String) {
viewerCountTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
Task {
try? await self?.updateViewerCount(broadcastId: broadcastId)
}
}
}
func updateViewerCount(broadcastId: String) async throws {
guard let url = URL(string: "\(baseURL)/api/broadcasts/\(broadcastId)/viewer-count") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let viewerUpdate = ViewerCountUpdate(viewerCount: currentViewerCount)
request.httpBody = try JSONEncoder().encode(viewerUpdate)
let (_, _) = try await URLSession.shared.data(for: request)
}
struct ViewerCountUpdate: Codable {
let viewerCount: Int
}
func stopViewerCountTracking() {
viewerCountTimer?.invalidate()
viewerCountTimer = nil
}
When stopping the broadcast, set status to completed.
func stopBroadcast(broadcastId: String) async throws {
// Stop LiveKit publishing
await room?.localParticipant?.setCameraEnabled(false)
await room?.localParticipant?.setMicrophoneEnabled(false)
await room?.disconnect()
room = nil
// Stop timers
stopHeartbeat()
stopViewerCountTracking()
// Set broadcast status to completed
guard let url = URL(string: "\(baseURL)/api/broadcasts/\(broadcastId)/status") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let statusUpdate = StatusUpdateRequest(status: "completed")
request.httpBody = try JSONEncoder().encode(statusUpdate)
let (_, _) = try await URLSession.shared.data(for: request)
print("✅ Broadcast ended and marked as completed")
}
Full implementation combining all steps:
import Foundation
import LiveKit
class PubfuseBroadcastManager {
private let baseURL: String
private var room: Room?
private var broadcastId: String?
private var streamId: String?
private var isActive = false
private var heartbeatTimer: Timer?
private var viewerCountTimer: Timer?
private var currentViewerCount: Int = 0
init(baseURL: String = "https://www.pubfuse.com") {
self.baseURL = baseURL
}
// MARK: - Main Broadcast Flow
/// Complete flow: Create → Start Publishing → Activate
func startBroadcast(title: String) async throws {
// Step 1: Create broadcast
print("📝 Step 1: Creating broadcast...")
let broadcast = try await createBroadcast(title: title)
self.broadcastId = broadcast.id
self.streamId = broadcast.streamId
print("✅ Broadcast created: \(broadcast.id)")
// Step 2: Start LiveKit publishing
print("📹 Step 2: Starting LiveKit publishing...")
try await startLiveKitPublishing(streamId: broadcast.streamId)
print("✅ LiveKit publishing started")
// Step 3: Set broadcast to active ⚠️ CRITICAL
print("🚀 Step 3: Activating broadcast...")
try await setBroadcastActive(broadcastId: broadcast.id)
self.isActive = true
print("✅ Broadcast is now ACTIVE and discoverable!")
// Step 4: Start heartbeat and viewer tracking
print("💓 Step 4: Starting heartbeat and viewer tracking...")
startHeartbeat(broadcastId: broadcast.id)
startViewerCountTracking(broadcastId: broadcast.id)
print("✅ All systems running!")
}
/// Stop broadcast and cleanup
func stopBroadcast() async throws {
guard let broadcastId = broadcastId else { return }
print("⏹️ Stopping broadcast...")
// Stop LiveKit
await room?.localParticipant?.setCameraEnabled(false)
await room?.localParticipant?.setMicrophoneEnabled(false)
await room?.disconnect()
room = nil
// Stop timers
stopHeartbeat()
stopViewerCountTracking()
// Set to completed
try await setBroadcastCompleted(broadcastId: broadcastId)
self.isActive = false
print("✅ Broadcast stopped")
}
// MARK: - API Methods
private func createBroadcast(title: String) async throws -> BroadcastResponse {
guard let url = URL(string: "\(baseURL)/api/broadcasts") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let broadcastRequest = CreateBroadcastRequest(
title: title,
visibility: "public",
tags: ["live", "broadcast"],
metadata: [
"createdBy": "iOS-App",
"deviceModel": UIDevice.current.model
]
)
request.httpBody = try JSONEncoder().encode(broadcastRequest)
let (data, _) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(BroadcastResponse.self, from: data)
}
private func startLiveKitPublishing(streamId: String) async throws {
let tokenResponse = try await getLiveKitToken(streamId: streamId, role: "publisher")
let room = Room()
room.delegate = self
try await room.connect(
url: tokenResponse.serverUrl,
token: tokenResponse.token,
connectOptions: ConnectOptions(
autoManageVideo: true,
autoManageAudio: true
)
)
self.room = room
await room.localParticipant?.setCameraEnabled(true)
await room.localParticipant?.setMicrophoneEnabled(true)
}
private func setBroadcastActive(broadcastId: String) async throws {
try await updateBroadcastStatus(broadcastId: broadcastId, status: "active")
}
private func setBroadcastCompleted(broadcastId: String) async throws {
try await updateBroadcastStatus(broadcastId: broadcastId, status: "completed")
}
private func updateBroadcastStatus(broadcastId: String, status: String) async throws {
guard let url = URL(string: "\(baseURL)/api/broadcasts/\(broadcastId)/status") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let statusUpdate = StatusUpdateRequest(status: status)
request.httpBody = try JSONEncoder().encode(statusUpdate)
let (_, _) = try await URLSession.shared.data(for: request)
}
private func startHeartbeat(broadcastId: String) {
heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
Task {
try? await self?.sendHeartbeat(broadcastId: broadcastId)
}
}
}
private func sendHeartbeat(broadcastId: String) async throws {
guard let url = URL(string: "\(baseURL)/api/sessions/\(broadcastId)/heartbeat") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let (_, _) = try await URLSession.shared.data(for: request)
}
private func stopHeartbeat() {
heartbeatTimer?.invalidate()
heartbeatTimer = nil
}
private func startViewerCountTracking(broadcastId: String) {
viewerCountTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
Task {
try? await self?.updateViewerCount(broadcastId: broadcastId)
}
}
}
private func updateViewerCount(broadcastId: String) async throws {
guard let url = URL(string: "\(baseURL)/api/broadcasts/\(broadcastId)/viewer-count") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let viewerUpdate = ViewerCountUpdate(viewerCount: currentViewerCount)
request.httpBody = try JSONEncoder().encode(viewerUpdate)
let (_, _) = try await URLSession.shared.data(for: request)
}
private func stopViewerCountTracking() {
viewerCountTimer?.invalidate()
viewerCountTimer = nil
}
}
// MARK: - LiveKit Delegate
extension PubfuseBroadcastManager: RoomDelegate {
func room(_ room: Room, didConnect isReconnect: Bool) {
print("✅ Connected to LiveKit room")
}
func room(_ room: Room, didDisconnect error: Error?) {
print("❌ Disconnected from LiveKit room")
}
func room(_ room: Room, participant: RemoteParticipant, didConnect isReconnect: Bool) {
currentViewerCount += 1
print("👋 Viewer joined - count: \(currentViewerCount)")
}
func room(_ room: Room, participant: RemoteParticipant, didDisconnect error: Error?) {
currentViewerCount = max(0, currentViewerCount - 1)
print("👋 Viewer left - count: \(currentViewerCount)")
}
}
POST /api/broadcasts/{id}/status with status: "active"active AFTER starting LiveKit publishing, not beforecompleted when ending to update analyticsPOST /api/broadcastsPOST /api/broadcasts/{id}/statusThe server automatically generates streamId, streamKey, rtmpUrl, and hlsUrl when you create a broadcast.
You don't need separate API calls. The POST /api/broadcasts endpoint does everything in one go!
When you call POST /api/broadcasts, the server:
StreamService.create():
streamId (UUID)streamKey (UUID without dashes)rtmpUrl: Uses INGEST_RTMP env varhlsUrl: {PLAYBACK_HLS_BASE}/{streamId}/index.m3u8streamId from step 1"created"POST /api/broadcasts| Field | Type | Description | Usage |
|---|---|---|---|
id |
String (UUID) | Broadcast session ID | Use for status updates, heartbeats, viewer counts |
streamId |
String (UUID) | Stream/Room ID | Use this to connect to LiveKit room! |
streamKey |
String | Stream key | For RTMP ingest (if using external encoder) |
rtmpUrl |
String | RTMP ingest URL | For RTMP streaming setup |
hlsUrl |
String | HLS playback URL | For HLS playback testing |
watchUrl |
String | Web watch URL | Share this link with viewers |
status |
String | Broadcast status | Initially "created", then "active", "completed" |
import Foundation
class PubfuseBroadcastManager {
private let baseURL: String
private var broadcastId: String?
private var streamId: String?
init(baseURL: String = "https://www.pubfuse.com") {
self.baseURL = baseURL
}
/// Create broadcast - This returns EVERYTHING you need
func createBroadcast(title: String) async throws -> BroadcastData {
guard let url = URL(string: "\(baseURL)/api/broadcasts") else {
throw PubfuseError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let createRequest = CreateBroadcastRequest(
title: title,
visibility: "public",
tags: ["live", "broadcast"],
metadata: [
"createdBy": "iOS-App",
"deviceModel": UIDevice.current.model,
"appVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
]
)
request.httpBody = try JSONEncoder().encode(createRequest)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.broadcastCreationFailed
}
let broadcastData = try JSONDecoder().decode(BroadcastData.self, from: data)
// Store for later use
self.broadcastId = broadcastData.id
self.streamId = broadcastData.streamId
print("✅ Broadcast created successfully!")
print(" Broadcast ID: \(broadcastData.id)")
print(" Stream ID: \(broadcastData.streamId ?? "nil")")
print(" Stream Key: \(broadcastData.streamKey ?? "nil")")
print(" RTMP URL: \(broadcastData.rtmpUrl ?? "nil")")
print(" HLS URL: \(broadcastData.hlsUrl ?? "nil")")
print(" Watch URL: \(broadcastData.watchUrl ?? "nil")")
return broadcastData
}
}
// MARK: - Data Models
struct CreateBroadcastRequest: Codable {
let title: String
let description: String?
let visibility: String
let tags: [String]?
let metadata: [String: String]?
}
struct BroadcastData: Codable {
let id: String // Broadcast session ID
let streamId: String? // Stream ID (LiveKit room name) ⚠️ USE THIS!
let title: String
let description: String?
let status: String
let streamKey: String? // Stream key for RTMP
let rtmpUrl: String? // RTMP ingest URL
let hlsUrl: String? // HLS playback URL
let watchUrl: String? // Web watch URL
let createdAt: String
}
id ≠ streamId: These are different UUIDs!
id: The broadcast session database record IDstreamId: The LiveKit room name (this is what you use to connect!)streamId or streamKey yourselfPOST /api/broadcasts responsestreamId for LiveKit: When connecting to LiveKit, use the streamId from the response// Complete flow from creation to live streaming
func startCompleteBroadcast(title: String) async throws {
// Step 1: Create broadcast (gets ALL URLs and IDs)
print("📝 Step 1: Creating broadcast...")
let broadcast = try await createBroadcast(title: title)
guard let streamId = broadcast.streamId else {
throw PubfuseError.missingStreamId
}
print("✅ Broadcast created with all data:")
print(" 🆔 Broadcast ID: \(broadcast.id)")
print(" 📹 Stream ID: \(streamId)")
print(" 🔑 Stream Key: \(broadcast.streamKey ?? "nil")")
print(" 📡 RTMP URL: \(broadcast.rtmpUrl ?? "nil")")
print(" 🎬 HLS URL: \(broadcast.hlsUrl ?? "nil")")
print(" 🔗 Watch URL: \(broadcast.watchUrl ?? "nil")")
// Step 2: Start LiveKit publishing (using streamId!)
print("\n📹 Step 2: Starting LiveKit publishing...")
try await startLiveKitPublishing(streamId: streamId)
print("✅ LiveKit publishing started")
// Step 3: Set broadcast to active
print("\n🚀 Step 3: Setting broadcast to ACTIVE...")
try await setBroadcastActive(broadcastId: broadcast.id)
print("✅ Broadcast is now ACTIVE and discoverable!")
// Step 4: Start maintenance tasks
print("\n💓 Step 4: Starting heartbeat and viewer tracking...")
startHeartbeat(broadcastId: broadcast.id)
startViewerCountTracking(broadcastId: broadcast.id)
print("✅ All systems running!")
print("\n🎉 Broadcast is LIVE!")
print("🔗 Watch at: \(broadcast.watchUrl ?? "N/A")")
}
iOS App Server
| |
| POST /api/broadcasts |
| { title: "..." } |
| -----------------------------> |
| |
| Creates Stream (streamId, |
| streamKey, URLs) |
| Creates Broadcast Session |
| (id, links to streamId) |
| |
| <---------------------------- |
| { id, streamId, streamKey, |
| rtmpUrl, hlsUrl, watchUrl } |
| |
| POST /api/streaming/sessions/ |
| {streamId}/token |
| { userId, role: "publisher" } |
| -----------------------------> |
| |
| <---------------------------- |
| { token, serverUrl, room } |
| |
| Connect to LiveKit |
| (using streamId as room) |
| -----------------------------> |
| |
| POST /api/broadcasts/{id}/ |
| status |
| { status: "active" } |
| -----------------------------> |
| |
| Broadcast is now LIVE! |
| Viewers can watch at watchUrl |
| |
The watch URL returned by the server follows this pattern:
https://www.pubfuse.com/streams/{streamId}/watch
Or for the simplified version: https://www.pubfuse.com/watch/{streamId}
Sync your device contacts and automatically discover which contacts are already using Pubfuse. Follow them with one tap!
Upload and sync contacts from your device to discover which contacts are already on Pubfuse.
// Sync contacts from device
const syncContacts = async (contacts) => {
const response = await fetch('/api/contacts/sync', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
contacts: contacts.map(contact => ({
phoneNumber: contact.phoneNumber,
displayName: contact.displayName,
firstName: contact.firstName,
lastName: contact.lastName,
email: contact.email
}))
})
});
const result = await response.json();
console.log(`Synced ${result.totalContacts} contacts, ${result.pubfuseUsers} are on Pubfuse`);
return result;
};
// Example usage
const deviceContacts = [
{
phoneNumber: "+1234567890",
displayName: "John Doe",
firstName: "John",
lastName: "Doe",
email: "[email protected]"
}
];
const syncResult = await syncContacts(deviceContacts);
Automatically follow contacts who are already on Pubfuse, or follow them manually from your contacts list.
// Follow a contact who's on Pubfuse
const followContact = async (contactId) => {
const response = await fetch(`/api/contacts/${contactId}/follow`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
console.log('Contact followed successfully');
}
};
// Get user's contacts with Pubfuse status
const getContacts = async () => {
const response = await fetch('/api/contacts', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const contacts = await response.json();
// Filter contacts who are on Pubfuse
const pubfuseContacts = contacts.filter(contact => contact.isPubfuseUser);
console.log(`${pubfuseContacts.length} of your contacts are on Pubfuse`);
return contacts;
};
Get complete user profiles with follower/following counts and social metrics.
// Get full user profile with social metrics
const getFullProfile = async () => {
const response = await fetch('/api/users/profile/full', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const profile = await response.json();
console.log(`Profile: ${profile.username} has ${profile.followerCount} followers`);
return profile;
};
// Search for users
const searchUsers = async (query) => {
const response = await fetch(`/api/users/search?q=${encodeURIComponent(query)}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const users = await response.json();
return users;
};
// Get follower/following counts
const getSocialCounts = async () => {
const [followersRes, followingRes] = await Promise.all([
fetch('/api/users/followers/count', {
headers: { 'Authorization': `Bearer ${token}` }
}),
fetch('/api/users/following/count', {
headers: { 'Authorization': `Bearer ${token}` }
})
]);
const followers = await followersRes.json();
const following = await followingRes.json();
return {
followers: followers.count,
following: following.count
};
};
All phone numbers are automatically normalized for consistent matching. Supports US/Canada (+1) and international formats.
POST /api/contacts/sync - Sync device contactsGET /api/contacts - Get user's contactsGET /api/contacts/:id - Get specific contactPOST /api/contacts/:id/follow - Follow contactGET /api/users/profile/full - Full profileGET /api/users/search - Search usersUpload and manage files with automatic thumbnail generation, multiple sizes, and rich metadata. Perfect for profile images, attachments, media files, and more!
Upload files with automatic size variants and rich metadata. Files are organized by user, category, and tags for easy retrieval.
// Upload a file
const uploadFile = async (fileData) => {
const response = await fetch('/api/files', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'profile-photo.jpg',
description: 'My profile picture',
mimeType: 'image/jpeg',
filePathOriginal: '/uploads/original/profile-photo.jpg',
filePathThumbnail: '/uploads/thumb/profile-photo.jpg',
filePathSmall: '/uploads/small/profile-photo.jpg',
filePathMedium: '/uploads/medium/profile-photo.jpg',
filePathLarge: '/uploads/large/profile-photo.jpg',
fileSizeBytes: 245890,
categoryName: 'profile',
tags: ['profilepic', 'avatar'],
location: 'San Francisco, CA'
})
});
return await response.json();
};
// Get user's profile image
const getProfileImage = async (userId) => {
const response = await fetch(`/api/files/profileimage/foruser/${userId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
return await response.json();
};
// Get all files for a user with optional filtering
const getUserFiles = async (userId, category = null, tag = null) => {
let url = `/api/files/foruser/${userId}`;
const params = new URLSearchParams();
if (category) params.append('category', category);
if (tag) params.append('tag', tag);
if (params.toString()) url += `?${params.toString()}`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
return await response.json();
};
// Update file metadata
const updateFile = async (fileId, updates) => {
const response = await fetch(`/api/files/${fileId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
return await response.json();
};
// Delete a file
const deleteFile = async (fileId) => {
const response = await fetch(`/api/files/${fileId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.status === 204; // No Content on success
};
// Get files for a broadcast
const getBroadcastFiles = async (broadcastId) => {
const response = await fetch(`/api/files/forbroadcast/${broadcastId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
return await response.json();
};
// Get files for a contact
const getContactFiles = async (contactId) => {
const response = await fetch(`/api/files/forcontact/${contactId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
return await response.json();
};
/api/files
Upload/create a new file record
/api/files
Get all files for authenticated user
/api/files/foruser/{userId}
Get files for a user with optional category/tag filters
/api/files/profileimage/foruser/{userId}
Quick access to user's profile image
/api/files/contactimage/foruser/{userId}
Quick access to contact's image
/api/files/forcontact/{contactId}
Get all files for a contact
/api/files/formessage/{messageId}
Get all files for a message
/api/files/forcall/{callId}
Get all files for a call
/api/files/forbroadcast/{broadcastId}
Get all files for a broadcast
/api/files/{id}
Update file metadata
/api/files/{id}
Delete a file
When uploading files, you can provide paths for multiple size variants. The system supports: original, thumbnail, small, medium, and large. Use the appropriate variant based on your UI context for optimal performance.
import PubfuseSDK
// Get SDK instance
guard let sdk = appViewModel.pubfuseSDK else { return }
// Schedule a new event
let startTime = Date().addingTimeInterval(3600) // 1 hour from now
let endTime = startTime.addingTimeInterval(1800) // 30 minutes duration
let request = PFScheduleEventRequest(
scheduledStartTime: startTime,
scheduledEndTime: endTime,
title: "My Scheduled Event",
description: "Event description",
repeatMode: "none",
repeatUntilEnd: false
)
do {
let event = try await sdk.eventsService.scheduleEvent(request)
print("Event scheduled: \(event.id)")
} catch {
print("Error scheduling event: \(error)")
}
// Get all events
let events = try await sdk.eventsService.getEvents()
// Get a specific event
let event = try await sdk.eventsService.getEvent(eventId: eventId)
// Update an event
let updateRequest = PFUpdateEventRequest(
scheduledStartTime: newStartTime,
scheduledEndTime: newEndTime,
title: "Updated Title",
description: "Updated description",
repeatMode: "daily",
repeatUntilEnd: true
)
let updatedEvent = try await sdk.eventsService.updateEvent(
eventId: eventId,
updateRequest
)
// Upload content for an event
let fileData = try Data(contentsOf: fileURL)
let uploadedEvent = try await sdk.eventsService.uploadEventContent(
eventId: eventId,
fileData: fileData,
mimeType: "video/mp4",
filename: "video.mp4"
)
// Manually start event playback
let startedEvent = try await sdk.eventsService.startVideoPlayback(eventId: eventId)
// Get event sync information (for synchronized playback)
let sync = try await sdk.eventsService.getEventSync(eventId: eventId)
print("Current position: \(sync.currentPosition)")
print("Playback started at: \(sync.playbackStartedAt)")
// Delete an event
let response = try await sdk.eventsService.deleteEvent(eventId: eventId)
print("Event deleted: \(response.success)")
/api/events
Get all scheduled events (public)
Query params: limit, offset, status/api/events/:id
Get a specific event by ID (public)
/api/events/:id/sync
Get event sync information for synchronized playback (public)
/api/events/schedule
Schedule a new event (requires authentication)
/api/events/:id
Update an existing event (requires authentication, owner only)
/api/events/:id
Delete an event (requires authentication, owner only)
/api/events/:id/upload
Upload content file for an event (requires authentication, owner only)
Multipart form data with file field. Supports video (mp4, mov, webm) and audio (mp3, wav, m4a, aac) files up to 1GB./api/events/:id/start-video
Manually start event playback (requires authentication, owner only)
Creates broadcast session, sends notifications, and starts LiveKit Egress./api/events/:id/takeover
Take over an active event stream (requires authentication, owner only)
repeatUntilEnd: When true and repeatMode is "until_end", the content will loop continuously until the scheduled end time is reached.
Scheduled events support synchronized playback across all viewers. The server tracks the current playback position and all clients sync to this position.
The sync algorithm ensures all viewers see the same content at the same time:
currentPosition = initialPosition + (now - playbackStartedAt)
GET /api/events/:id/sync to get the current sync positionplaybackStartedAt and currentPlaybackPositionScheduled events support multiple repeat modes for flexible scheduling:
repeatUntilEnd: true to enable looping.repeatMode is "until_end" and repeatUntilEnd is true, the content file will loop from the beginning when it reaches the end, continuing until the scheduled end time.When an event starts (either automatically or manually), the system will:
class PubfuseSDK {
constructor(apiKey, secretKey, baseUrl = 'https://api.pubfuse.com') {
this.apiKey = apiKey;
this.secretKey = secretKey;
this.baseUrl = baseUrl;
}
async makeRequest(method, path, data = null) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
const signature = this.generateSignature(method, path, body, timestamp);
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers: {
'X-API-Key': this.apiKey,
'X-Signature': signature,
'X-Timestamp': timestamp,
'Content-Type': 'application/json'
},
body: data ? body : undefined
});
return response.json();
}
generateSignature(method, path, body, timestamp) {
const crypto = require('crypto');
const payload = `${method}${path}${body}${timestamp}`;
return crypto.createHmac('sha256', this.secretKey)
.update(payload)
.digest('hex');
}
// Stream Management
async createSession(title, description = '') {
return this.makeRequest('POST', '/api/v1/sessions', {
title,
description
});
}
async getSessions() {
return this.makeRequest('GET', '/api/v1/sessions');
}
// User Management
async registerUser(userData) {
return this.makeRequest('POST', '/api/users/signup', userData);
}
async loginUser(email, password) {
return this.makeRequest('POST', '/api/users/login', {
email,
password
});
}
}
// Usage
const sdk = new PubfuseSDK('pk_your_api_key', 'sk_your_secret_key');
// Create a session
sdk.createSession('My Live Stream', 'Welcome to my stream!')
.then(session => console.log('Session created:', session))
.catch(error => console.error('Error:', error));
import requests
import hmac
import hashlib
import time
from typing import Dict, Any, Optional
class PubfuseSDK:
def __init__(self, api_key: str, secret_key: str, base_url: str = 'https://api.pubfuse.com'):
self.api_key = api_key
self.secret_key = secret_key
self.base_url = base_url
def _generate_signature(self, method: str, path: str, body: str, timestamp: str) -> str:
payload = f"{method}{path}{body}{timestamp}"
return hmac.new(
self.secret_key.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
def _make_request(self, method: str, path: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
timestamp = str(int(time.time()))
body = json.dumps(data) if data else ''
signature = self._generate_signature(method, path, body, timestamp)
headers = {
'X-API-Key': self.api_key,
'X-Signature': signature,
'X-Timestamp': timestamp,
'Content-Type': 'application/json'
}
response = requests.request(
method,
f"{self.base_url}{path}",
headers=headers,
json=data
)
return response.json()
def create_session(self, title: str, description: str = '') -> Dict[str, Any]:
return self._make_request('POST', '/api/v1/sessions', {
'title': title,
'description': description
})
def get_sessions(self) -> Dict[str, Any]:
return self._make_request('GET', '/api/v1/sessions')
def register_user(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
return self._make_request('POST', '/api/users/signup', user_data)
def login_user(self, email: str, password: str) -> Dict[str, Any]:
return self._make_request('POST', '/api/users/login', {
'email': email,
'password': password
})
# Usage
sdk = PubfuseSDK('pk_your_api_key', 'sk_your_secret_key')
# Create a session
try:
session = sdk.create_session('My Live Stream', 'Welcome to my stream!')
print('Session created:', session)
except Exception as e:
print('Error:', e)
Generate a LiveKit token from your server, then pass it unchanged (including the livekit_ prefix) to the client SDK.
# Get LiveKit token for a session (UUID or short ID like r5)
curl -s -X POST "http://127.0.0.1:8080/api/streaming/sessions/SESSION_ID/token" \
-H "Content-Type: application/json" \
-H "X-API-Key: <your_api_key>" \
-d '{"userId":"550e8400-e29b-41d4-a716-446655440000","role":"subscriber"}' | jq .
Minimal LiveKit Web client usage:
// tokenResponse: { token, room, serverUrl }
const room = new LiveKit.Room();
await room.connect(tokenResponse.serverUrl, tokenResponse.token, {
autoManageVideo: true,
autoManageAudio: true
});
room.on(LiveKit.RoomEvent.TrackSubscribed, (track, pub, participant) => {
if (track.kind === 'video') {
const el = document.getElementById('remoteVideo');
track.attach(el);
}
});
This is what the web watch page does at runtime. Mirror this sequence for mobile clients.
r5).window.LiveKit is available.GET /api/streaming/providers and select id === "livekit".POST /api/streaming/sessions/{id}/token with X-API-Key, role subscriber, and a UUID userId.livekit_ prefix) to the SDK Room.connect().TrackSubscribed, attach remote video/audio to the player.// Pseudocode that mirrors watch.leaf
const sessionId = getSessionIdFromUrl();
// 1) Provider
const providers = await fetch('/api/streaming/providers').then(r => r.json());
const livekit = providers.find(p => p.id === 'livekit' && p.isConfigured);
// 2) Token (server returns: { token, room, serverUrl, expiresAt })
const tRes = await fetch(`/api/streaming/sessions/${sessionId}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
body: JSON.stringify({ userId: USER_UUID, role: 'subscriber' })
});
const t = await tRes.json();
// 3) Connect
const room = new LiveKit.Room();
await room.connect(t.serverUrl || livekit.configuration.url, t.token);
// 4) Track handling
room.on(LiveKit.RoomEvent.TrackSubscribed, (track, pub, participant) => {
if (track.kind === 'video') track.attach(document.getElementById('remoteVideo'));
});
// 5) Reactions/chat (data)
function sendReaction(kind) {
// send via your server or LiveKit data channel depending on your design
}
livekit_ prefix; integer nbf/iat/exp; grants include roomJoin and room.Use your backend to mint the token, then connect the SDK on device.
import LiveKit
let room = Room()
// tokenResponse.serverUrl (wss://...), tokenResponse.token (starts with livekit_)
try await room.connect(tokenResponse.serverUrl, tokenResponse.token)
room.on(.trackSubscribed) { track, pub, participant in
if let video = track as? RemoteVideoTrack {
// Attach to your LKVideoView / UIView
}
}
import io.livekit.android.Room
val room = Room.getInstance(applicationContext)
// tokenResponse.serverUrl (wss://...), tokenResponse.token (starts with livekit_)
room.connect(tokenResponse.serverUrl, tokenResponse.token)
room.onTrackSubscribed = { track, publication, participant ->
// Attach video track to SurfaceViewRenderer
}
livekit_ prefix from the token. The server includes integer nbf/iat/exp and grants (e.g., roomJoin, room, canSubscribe).
import Foundation
import CryptoKit
class PubfuseSDK {
private let apiKey: String
private let secretKey: String
private let baseURL: String
init(apiKey: String, secretKey: String, baseURL: String = "https://api.pubfuse.com") {
self.apiKey = apiKey
self.secretKey = secretKey
self.baseURL = baseURL
}
private func generateSignature(method: String, path: String, body: String, timestamp: String) -> String {
let payload = "\(method)\(path)\(body)\(timestamp)"
let key = SymmetricKey(data: secretKey.data(using: .utf8)!)
let signature = HMAC.authenticationCode(for: payload.data(using: .utf8)!, using: key)
return Data(signature).map { String(format: "%02hhx", $0) }.joined()
}
func makeRequest(method: String, path: String, data: T? = nil) async throws -> Data {
let timestamp = String(Int(Date().timeIntervalSince1970))
let body = data != nil ? try JSONEncoder().encode(data) : Data()
let bodyString = String(data: body, encoding: .utf8) ?? ""
let signature = generateSignature(method: method, path: path, body: bodyString, timestamp: timestamp)
var request = URLRequest(url: URL(string: "\(baseURL)\(path)")!)
request.httpMethod = method
request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
request.setValue(signature, forHTTPHeaderField: "X-Signature")
request.setValue(timestamp, forHTTPHeaderField: "X-Timestamp")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if data != nil {
request.httpBody = body
}
let (data, _) = try await URLSession.shared.data(for: request)
return data
}
// Stream Management
func createSession(title: String, description: String = "") async throws -> SessionResponse {
let request = CreateSessionRequest(title: title, description: description)
let data = try await makeRequest(method: "POST", path: "/api/v1/sessions", data: request)
return try JSONDecoder().decode(SessionResponse.self, from: data)
}
func getSessions() async throws -> [SessionResponse] {
let data = try await makeRequest(method: "GET", path: "/api/v1/sessions")
return try JSONDecoder().decode([SessionResponse].self, from: data)
}
}
// Usage
let sdk = PubfuseSDK(apiKey: "pk_your_api_key", secretKey: "sk_your_secret_key")
Task {
do {
let session = try await sdk.createSession(title: "My Live Stream", description: "Welcome!")
print("Session created: \(session)")
} catch {
print("Error: \(error)")
}
}
Each SDK Client has a default rate limit of 1000 requests per hour. Monitor your usage and contact support if you need higher limits.
// Implement exponential backoff for rate limiting
async function makeRequestWithRetry(requestFn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await requestFn();
} catch (error) {
if (error.status === 429 && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
Complete implementation examples for iOS apps using the Pubfuse contacts and follow features.
Implement contact synchronization in your iOS app to discover Pubfuse users.
import Foundation
import Contacts
class PubfuseContactsManager: ObservableObject {
private let apiBaseURL = "https://www.pubfuse.com/api"
private var authToken: String?
// MARK: - Contact Sync
func syncContacts() async throws -> ContactsSyncResponse {
let contacts = try await fetchDeviceContacts()
let syncRequest = ContactsSyncRequest(
contacts: contacts.map { contact in
ContactSyncRequest(
phoneNumber: contact.phoneNumber ?? "",
displayName: contact.displayName,
firstName: contact.firstName,
lastName: contact.lastName,
email: contact.email
)
}
)
let url = URL(string: "\(apiBaseURL)/contacts/sync")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
request.httpBody = try JSONEncoder().encode(syncRequest)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.networkError("Failed to sync contacts")
}
return try JSONDecoder().decode(ContactsSyncResponse.self, from: data)
}
private func fetchDeviceContacts() async throws -> [DeviceContact] {
let store = CNContactStore()
guard try await store.requestAccess(for: .contacts) else {
throw PubfuseError.permissionDenied
}
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey,
CNContactPhoneNumbersKey, CNContactEmailAddressesKey] as [CNKeyDescriptor]
let request = CNContactFetchRequest(keysToFetch: keys)
var contacts: [DeviceContact] = []
try store.enumerateContacts(with: request) { contact, _ in
let phoneNumber = contact.phoneNumbers.first?.value.stringValue
let email = contact.emailAddresses.first?.value as String?
let deviceContact = DeviceContact(
firstName: contact.givenName,
lastName: contact.familyName,
displayName: CNContactFormatter.string(from: contact, style: .fullName),
phoneNumber: phoneNumber,
email: email
)
contacts.append(deviceContact)
}
return contacts
}
}
// MARK: - Data Models
struct ContactsSyncRequest: Codable {
let contacts: [ContactSyncRequest]
}
struct ContactSyncRequest: Codable {
let phoneNumber: String
let displayName: String?
let firstName: String?
let lastName: String?
let email: String?
}
struct ContactsSyncResponse: Codable {
let success: Bool
let message: String
let contacts: [ContactResponse]
let totalContacts: Int
let pubfuseUsers: Int
}
struct ContactResponse: Codable {
let id: UUID
let phoneNumber: String
let displayName: String?
let firstName: String?
let lastName: String?
let email: String?
let isPubfuseUser: Bool
let pubfuseUserId: UUID?
let isFollowing: Bool
let isFollowedBy: Bool
let createdAt: Date
let updatedAt: Date
}
struct DeviceContact {
let firstName: String
let lastName: String
let displayName: String?
let phoneNumber: String?
let email: String?
}
enum PubfuseError: Error {
case networkError(String)
case permissionDenied
case invalidToken
}
Implement following and unfollowing of contacts who are on Pubfuse.
// MARK: - Follow Management
func followContact(contactId: UUID) async throws {
let url = URL(string: "\(apiBaseURL)/contacts/\(contactId)/follow")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
let (_, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw PubfuseError.networkError("Invalid response")
}
switch httpResponse.statusCode {
case 200:
// Successfully followed
break
case 400:
throw PubfuseError.networkError("Contact is not a Pubfuse user")
case 409:
throw PubfuseError.networkError("Already following this user")
default:
throw PubfuseError.networkError("Failed to follow contact")
}
}
func unfollowContact(contactId: UUID) async throws {
let url = URL(string: "\(apiBaseURL)/contacts/\(contactId)/follow")!
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
let (_, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.networkError("Failed to unfollow contact")
}
}
// MARK: - User Profile Enhancement
func getFullProfile() async throws -> FullUserProfileResponse {
let url = URL(string: "\(apiBaseURL)/users/profile/full")!
var request = URLRequest(url: url)
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.networkError("Failed to get profile")
}
return try JSONDecoder().decode(FullUserProfileResponse.self, from: data)
}
struct FullUserProfileResponse: Codable {
let id: UUID
let username: String
let email: String
let phoneNumber: String
let firstName: String?
let lastName: String?
let avatarUrl: String?
let isActive: Bool
let emailVerified: Bool
let phoneVerified: Bool
let appIdName: String
let followerCount: Int
let followingCount: Int
let connectionCount: Int
let createdAt: Date
let updatedAt: Date
}
Implement user search functionality to find and connect with other Pubfuse users.
// MARK: - User Search
func searchUsers(query: String, limit: Int = 20) async throws -> [UserSearchResponse] {
let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
let url = URL(string: "\(apiBaseURL)/users/search?q=\(encodedQuery)&limit=\(limit)")!
var request = URLRequest(url: url)
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.networkError("Failed to search users")
}
return try JSONDecoder().decode([UserSearchResponse].self, from: data)
}
struct UserSearchResponse: Codable {
let id: UUID
let username: String
let email: String
let firstName: String?
let lastName: String?
let avatarUrl: String?
let isFollowing: Bool
}
Upload and retrieve poster images for broadcast sessions. Posters are displayed in broadcast lists and serve as preview thumbnails.
// MARK: - Broadcast Poster Images
// Get the file service
guard let sdk = pubfuseSDK else { return }
let fileService = PFFileService(networkService: sdk.networkService)
// Upload a poster image for a broadcast
func uploadBroadcastPoster(broadcastId: String, image: UIImage) async {
do {
let posterFile = try await fileService.uploadBroadcastPosterImage(
image: image,
broadcastId: broadcastId,
description: "Poster for my live stream"
)
print("✅ Poster uploaded: \(posterFile.id ?? "unknown")")
print(" Thumbnail: \(posterFile.filePathThumbnail ?? "N/A")")
print(" Small: \(posterFile.filePathSmall ?? "N/A")")
print(" Original: \(posterFile.filePathOriginal ?? "N/A")")
} catch {
print("❌ Failed to upload poster: \(error)")
}
}
// Get poster image for a broadcast
func loadBroadcastPoster(broadcastId: String) async {
do {
guard let posterFile = try await fileService.getBroadcastPosterImage(for: broadcastId) else {
print("ℹ️ No poster image found for broadcast")
return
}
// Get the image URL (prefer smallest size for list views)
let imagePath = posterFile.filePathThumbnail ??
posterFile.filePathSmall ??
posterFile.filePathMedium ??
posterFile.filePathOriginal
let config = pubfuseSDK?.configuration
let baseURL = config?.baseURL ?? "http://localhost:8080"
let imageURL = "\(baseURL)/\(imagePath)"
print("✅ Poster image URL: \(imageURL)")
// Use imageURL with CachedAsyncImage in SwiftUI
} catch {
print("❌ Failed to load poster: \(error)")
}
}
// Display poster in SwiftUI view
struct BroadcastPosterView: View {
let broadcastId: String
@State private var posterImageUrl: String?
@EnvironmentObject var appViewModel: AppViewModel
var body: some View {
ZStack {
// Placeholder
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
// Poster image
if let posterImageUrl = posterImageUrl, !posterImageUrl.isEmpty {
CachedAsyncImage(url: posterImageUrl) {
RoundedRectangle(cornerRadius: 10)
.fill(Color.gray.opacity(0.3))
}
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 75)
.clipShape(RoundedRectangle(cornerRadius: 10))
.id(posterImageUrl) // Force re-render when URL changes
}
}
.onAppear {
loadPosterImage()
}
}
private func loadPosterImage() {
guard let fileService = appViewModel.pubfuseSDK?.fileService else { return }
Task {
do {
let posterFile = try await fileService.getBroadcastPosterImage(for: broadcastId)
await MainActor.run {
if let imageFile = posterFile {
let imagePath = imageFile.filePathThumbnail ??
imageFile.filePathSmall ??
imageFile.filePathMedium ??
imageFile.filePathOriginal
let config = appViewModel.pubfuseSDK?.configuration
let baseURL = config?.baseURL ?? "http://localhost:8080"
self.posterImageUrl = "\(baseURL)/\(imagePath)"
}
}
} catch {
print("Failed to load poster: \(error)")
}
}
}
}
filePathThumbnail or filePathSmall for list views (faster loading)filePathMedium or filePathLarge for detail viewsCachedAsyncImage with .id() modifier for proper refresh behaviorforBroadcastIdAllow users to upload a poster image before starting a broadcast:
struct GoLiveView: View {
@State private var selectedPosterImage: UIImage?
@State private var showingImagePicker = false
@State private var fileService: PFFileService?
var body: some View {
VStack {
// Poster upload section
if let posterImage = selectedPosterImage {
HStack {
Image(uiImage: posterImage)
.resizable()
.frame(width: 80, height: 60)
.clipShape(RoundedRectangle(cornerRadius: 8))
Button("Remove") {
selectedPosterImage = nil
}
}
} else {
Button("Upload Poster Image") {
showingImagePicker = true
}
}
// Start streaming button
Button("Start Streaming") {
startStreaming()
}
}
.sheet(isPresented: $showingImagePicker) {
ImagePicker(image: $selectedPosterImage)
}
}
private func startStreaming() async {
// Create broadcast
let broadcast = try await appViewModel.startLiveKitBroadcast(...)
// Upload poster if selected
if let posterImage = selectedPosterImage,
let fileService = fileService,
let broadcastId = broadcast.id {
do {
_ = try await fileService.uploadBroadcastPosterImage(
image: posterImage,
broadcastId: broadcastId,
description: "Poster for broadcast"
)
print("✅ Poster uploaded")
} catch {
print("⚠️ Failed to upload poster: \(error)")
}
}
}
}
Upload and manage files in your iOS app, including profile images and file attachments.
// MARK: - File Management
import SwiftUI
import PhotosUI
class PubfuseFileManager: ObservableObject {
private let apiBaseURL = "https://www.pubfuse.com/api"
private var authToken: String?
// Upload a file
func uploadFile(fileData: FileUploadData) async throws -> PFFile {
let createRequest = PFFileCreateRequest(
name: fileData.name,
description: fileData.description,
mimeType: fileData.mimeType,
filePathOriginal: fileData.pathOriginal,
forContactID: fileData.forContactID,
filePathThumbnail: fileData.pathThumbnail,
filePathSmall: fileData.pathSmall,
filePathMedium: fileData.pathMedium,
filePathLarge: fileData.pathLarge,
fileSizeBytes: fileData.sizeBytes,
categoryName: fileData.categoryName,
tags: fileData.tags,
location: fileData.location,
forMessageId: fileData.forMessageId,
forCallId: fileData.forCallId,
forBroadcastId: fileData.forBroadcastId
)
let url = URL(string: "\(apiBaseURL)/files")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
request.httpBody = try JSONEncoder().encode(createRequest)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.networkError("Failed to upload file")
}
return try JSONDecoder().decode(PFFile.self, from: data)
}
// Upload profile image with multiple sizes
func uploadProfileImage(originalPath: String, thumbnailPath: String, sizeBytes: Int64) async throws -> PFFile {
let fileData = FileUploadData(
name: "profile-photo.jpg",
description: "Profile picture",
mimeType: "image/jpeg",
pathOriginal: originalPath,
pathThumbnail: thumbnailPath,
pathSmall: nil,
pathMedium: nil,
pathLarge: nil,
sizeBytes: sizeBytes,
categoryName: "profile",
tags: ["profilepic", "avatar"],
location: nil,
forContactID: nil,
forMessageId: nil,
forCallId: nil,
forBroadcastId: nil
)
return try await uploadFile(fileData: fileData)
}
// Get user's profile image
func getProfileImage(for userId: String) async throws -> PFFile {
let url = URL(string: "\(apiBaseURL)/files/profileimage/foruser/\(userId)")!
var request = URLRequest(url: url)
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.networkError("Profile image not found")
}
return try JSONDecoder().decode(PFFile.self, from: data)
}
// Get all user files with optional filters
func getUserFiles(userId: String, category: String? = nil, tag: String? = nil) async throws -> [PFFile] {
var urlString = "\(apiBaseURL)/files/foruser/\(userId)"
var queryParams: [String] = []
if let category = category {
queryParams.append("category=\(category)")
}
if let tag = tag {
queryParams.append("tag=\(tag)")
}
if !queryParams.isEmpty {
urlString += "?" + queryParams.joined(separator: "&")
}
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.networkError("Failed to get files")
}
return try JSONDecoder().decode([PFFile].self, from: data)
}
// Update file metadata
func updateFile(fileId: String, updates: PFFileUpdateRequest) async throws -> PFFile {
let url = URL(string: "\(apiBaseURL)/files/\(fileId)")!
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
request.httpBody = try JSONEncoder().encode(updates)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw PubfuseError.networkError("Failed to update file")
}
return try JSONDecoder().decode(PFFile.self, from: data)
}
// Delete a file
func deleteFile(fileId: String) async throws {
let url = URL(string: "\(apiBaseURL)/files/\(fileId)")!
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
request.setValue("Bearer \(authToken ?? "")", forHTTPHeaderField: "Authorization")
let (_, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 204 else {
throw PubfuseError.networkError("Failed to delete file")
}
}
}
// MARK: - File Models
struct PFFile: Codable {
let id: String
let userID: String
let forContactID: String?
let name: String
let description: String?
let mimeType: String
let filePathOriginal: String
let filePathThumbnail: String?
let filePathSmall: String?
let filePathMedium: String?
let filePathLarge: String?
let fileSizeBytes: Int64
let categoryName: String?
let tags: [String]?
let location: String?
let forMessageId: String?
let forCallId: String?
let forBroadcastId: String?
let createdAt: Date?
let updatedAt: Date?
}
struct PFFileCreateRequest: Codable {
let name: String
let description: String?
let mimeType: String
let filePathOriginal: String
let forContactID: String?
let filePathThumbnail: String?
let filePathSmall: String?
let filePathMedium: String?
let filePathLarge: String?
let fileSizeBytes: Int64
let categoryName: String?
let tags: [String]?
let location: String?
let forMessageId: String?
let forCallId: String?
let forBroadcastId: String?
}
struct PFFileUpdateRequest: Codable {
let name: String?
let description: String?
let tags: [String]?
let location: String?
let categoryName: String?
}
struct FileUploadData {
let name: String
let description: String?
let mimeType: String
let pathOriginal: String
let pathThumbnail: String?
let pathSmall: String?
let pathMedium: String?
let pathLarge: String?
let sizeBytes: Int64
let categoryName: String?
let tags: [String]?
let location: String?
let forContactID: String?
let forMessageId: String?
let forCallId: String?
let forBroadcastId: String?
}
// Example: SwiftUI view for uploading a profile image
struct ProfileImageUploadView: View {
@StateObject private var fileManager = PubfuseFileManager()
@State private var selectedImage: UIImage?
@State private var showingImagePicker = false
@State private var uploadProgress = false
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack(spacing: 20) {
if let image = selectedImage {
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(Circle())
} else {
Image(systemName: "person.circle")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.foregroundColor(.gray)
}
Button("Select Photo") {
showingImagePicker = true
}
.buttonStyle(.bordered)
if uploadProgress {
ProgressView("Uploading...")
} else {
Button("Upload Profile Image") {
uploadProfileImage()
}
.buttonStyle(.borderedProminent)
.disabled(selectedImage == nil)
}
}
.sheet(isPresented: $showingImagePicker) {
ImagePicker(selectedImage: $selectedImage)
}
.padding()
}
private func uploadProfileImage() {
guard let image = selectedImage else { return }
uploadProgress = true
Task {
do {
// In a real implementation, you would:
// 1. Save the image to your server/storage (e.g., S3, local storage)
// 2. Get the storage paths for original and thumbnail
// 3. Calculate file size
// 4. Call uploadFile with the paths
// Example assuming you have the paths
let originalPath = "/storage/original/profile-\(UUID().uuidString).jpg"
let thumbnailPath = "/storage/thumb/profile-\(UUID().uuidString).jpg"
let fileSize: Int64 = 245890 // Actual file size
_ = try await fileManager.uploadProfileImage(
originalPath: originalPath,
thumbnailPath: thumbnailPath,
sizeBytes: fileSize
)
DispatchQueue.main.async {
uploadProgress = false
presentationMode.wrappedValue.dismiss()
}
} catch {
DispatchQueue.main.async {
uploadProgress = false
print("Upload failed: \(error)")
}
}
}
}
}
Error: 401 Unauthorized - Invalid API key
Solution:
X-API-Key headerError: 429 Too Many Requests
Solution:
Error: 401 Unauthorized - Invalid signature
Solution:
Enable debug logging to troubleshoot API issues:
// Add debug logging to your SDK
class PubfuseSDK {
constructor(apiKey, secretKey, debug = false) {
this.apiKey = apiKey;
this.secretKey = secretKey;
this.debug = debug;
}
log(message, data = null) {
if (this.debug) {
console.log(`[PubfuseSDK] ${message}`, data);
}
}
async makeRequest(method, path, data = null) {
this.log(`Making request: ${method} ${path}`, data);
// ... rest of implementation
this.log('Response received:', response);
return response;
}
}
If you need assistance integrating the Pubfuse SDK, we're here to help!