Tutorial
1. Prerequisites
-
Requirements
- iOS 14 or higher
- Xcode 13.4.1 or higher
- Swift 5.6.1 or higher
-
Creating Application
- To use the SDK, you must first sign up for a membership in the user console on the web and then create an application. Direct membership is currently limited. If you would like to sign up, please contact Jocoos.
-
Getting access token from server
- You need an access token to use the SDK. The application server uses the FlipFlop Cloud API to get an access token and passes it to the client
- For more information on using the API, refer the FlipFlop Cloud - Member Login API documentation.
- For more information about access tokens, see the Authentication section of Core Concepts
2. Installing SDK
-
Add the following to your Podfile
target ‘YourProject’ do
use_frameworks!
# Pods for FlipFlopSDK
pod 'FlipFlopLiteSDK', '1.8.0'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
end
end
end -
The SDK requires app permissions to use it. Please add the following permissions to your info.plist.
Privacy - Bluetooth Always Usage Description
Privacy - Camera Usage Description
Privacy - Microphone Usage Description -
In AppDelegate.swift, you'll need to set an AVAudioSession on app startup.
import AVFoundation
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
try session.setActive(true)
} catch {
print(error)
}
3. Initializing SDK
Before you can use the features provided by the SDK, you'll need to initialize it. Use .dev to connect to a development server for testing, and .prod to connect to a production server when you're done with development.
For the convenience of your development, FlipFlop Cloud provides two servers: one for development (.dev) and testing, and one for production (.prod). The production server is the server that runs with the highest priority for stability, while the development server is where new features can be added and tested.
import FlipFlopLiteSDK
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// connect to flipflop dev server
let config = FFLConfig(serverConfig: .dev)
FlipFlopLite.initialize(config: config)
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
try session.setActive(true)
} catch {
print(error)
}
}
4. Streaming Live
- Create a StreamingController and create an FFLStreamer instance for live broadcasting. In ACCESS_TOKEN, put the token you received through the FlipFlop Cloud API.
class StreamerViewController: UIViewController {
let streamer = FlipFlopLite.getStreamer(accessToken: ACCESS_TOKEN)
}
-
Call prepare to initialize the FFLStreamer. The prepare() function takes a UIView as its first parameter and an FFStreamerConfig to control the contents of the stream as its second parameter.
The preview, which is a UIView, displays the camera screen.
- You can set the following in FFStreamerConfig
Key Description default value preset video width와 height hd1280x720 videoBitrate video bitrate 2000 * 1024 keyFrameInterval key frame interval 2 fps framerate 30 sampleRate audio samplerate 48000 audioBitrate audio bitrate 64 * 1024 cameraPos camera position(front or back) .front
class StreamerViewController: UIViewController {
@IBOutlet weak var preview: UIView!
let streamer = FlipFlopLite.getStreamer(accessToken: ACCESS_TOKEN)
override func viewDidLoad() {
super.viewDidLoad()
self.preview.bounds = self.view.bounds // full screen view
let config = FFStreamerConfig()
config.videoBitrate = 2500 * 1000
config.cameraPos = .back
self.streamer.prepare(preview: self.preview, config: config)
}
}
- Setting Live Title
- You can set the title of the live to be shown when the live list is imported.
let title = "This is my first live!"
self.streamer.setVideoInfo(title: title)
-
Connecting an FFLStreamerDelegate to a StreamingController to receive SDK status information
- When using FFLStreamer, you can receive information about its internal state via events. You can use this information to check if your live stream is working properly, for example.
- The contents of FFLStreamerDelegate are shown below. The thread where the delegate is called may not be the main thread, so you should use DispatchQueue.main to call UI-related parts of the delegate.
/// > Notice: The thread which the delegate will be called on, is not guranteed to be the `main` thread.
/// If you will perform any UI update from the delegate, ensure the execution is from the `main` thread.
///
/// ## Example usage
/// ```swift
/// func fflStreamer(_ streamer: FFLStreamer, didUpdateStreamerState streamerState: StreamerState) {
/// DispatchQueue.main.async {
/// // update UI here
/// }
/// }
/// ```
public protocol FFLStreamerDelegate: AnyObject {
// StreamerState is updated
func fflStreamer(_ streamer: FFLStreamer, didUpdate streamerState: StreamerState)
// BroadcastState is updated
func fflStreamer(_ streamer: FFLStreamer, didUpdate broadcastState: BroadcastState)
// room for live streaming already exists
func fflStreamer(_ streamer: FFLStreamer, didExist videoRoom: FFLVideoRoom)
// alarm is published
func fflStreamer(_ streamer: FFLStreamer, didPublish alarmState: AlarmState)
// video bitrate is changed
func fflStreamer(_ streamer: FFLStreamer, didChangeVideoBitrate bitrate: Int)
// camera zoom is changed
func fflStreamer(_ streamer: FFLStreamer, didChangeZoom zoomFactor: CGFloat)
// channel is opened: can send chat messages after this event
func fflStreamer(_ streamer: FFLStreamer, didOpenChannel channelId: UInt64)
// chat message is received
func fflStreamer(_ streamer: FFLStreamer, didReceive message: FFLMessage)
// Some error happened
func fflStreamer(_ streamer: FFLStreamer, didFail error: FFError)
}-
StreamerState: FFLStreamer State
state description prepared notify you that you're ready to go live. started notify you that your live stream has started. This status does not mean that viewers can see you live. stopped notify you that the live stream has been interrupted. closed notify you that the live has ended. -
BroadcastState: Live State
state description active notify you that the live is in progress (viewers can see the live). inactive notify you that your live has been interrupted (viewers are unable to watch your live).
Difference between StreamerState and BroadcastState: StreamerState refers to the state of the FFLStreamer locally, while BroadcastState refers to whether the media server is streaming normally.
-
AlarmState: Provides status information about the live streaming
The AlarmState received from alarm events currently has three levels, with higher numbers indicating worse conditions. The values of the AlarmState are not called in order: if the transmission suddenly goes bad, alert_3 might be called first.
state description normal live streaming is ok alert_1 the state of live streaming is slightly poor alert_2 live state is worse than alert_1 alert_3 live state is worse than alert_2
- Connect to the StreamingController like this
extension StreamingController: FFLStreamerDelegate {
func fflStreamer(_ streamer: FFLStreamer, didUpdate streamerState: StreamerState) {
print("[STREAMER] didUpdateStreamerState: \(streamerState)")
switch streamerState {
case .prepared:
print("streamer is prepared")
case .started:
print("streamer is started")
retryCount = 0
retryTimer?.invalidate()
retryTimer = nil
case .stopped:
print("streamer is stopped")
case .closed:
print("streamer is closed")
default:
print("unknown streamState")
}
}
func fflStreamer(_ streamer: FFLStreamer, didUpdate broadcastState: BroadcastState) {
print("[STREAMER] didUpdateBroadcastState: \(broadcastState)")
switch broadcastState {
case .active:
print("broadcasting is active")
case .inactive:
print("broadcasting is inactive")
default:
print("unknown BroadcastState: \(broadcastState)")
}
}
func fflStreamer(_ streamer: FFLStreamer, didExist videoRoom: FFLVideoRoom) {
print("[STREAMER] didExistStartedVideoRoom: \(videoRoom.id)")
// restart previous live streaming
self.fflStreamer?.restart(videoRoomId: videoRoom.id)
}
func fflStreamer(_ streamer: FFLStreamer, didPublish alarmState: AlarmState) {
print("[STREAMER] didPublishAlarm: \(alarmState)")
}
func fflStreamer(_ streamer: FFLStreamer, didChangeVideoBitrate bitrate: Int) {
print("[STREAMER] didChangeVideoBitrate: \(bitrate)")
}
func fflStreamer(_ streamer: FFLStreamer, didChangeZoom zoomFactor: CGFloat) {
print("[STREAMER] didChangeZoom: \(zoomFactor)")
}
func fflStreamer(_ streamer: FFLStreamer, didOpenChannel channelId: UInt64) {
print("[STREAMER] didOpenChannel: \(channelId)")
}
func fflStreamer(_ streamer: FFLStreamer, didReceive message: FFLMessage) {
print("[STREAMER] didReceiveMessage: ")
}
func fflStreamer(_ streamer: FFLStreamer, didFail error: FFError) {
print("[STREAMER] didFail: \(error.code) / \(error.message)")
}
} -
When you start a live transmission, you call the enter() and start() functions. The enter() function is called first to tell the FlipFlop Cloud server that you want to go live. Then, you call the start() function to start streaming to the media server.
Checking if you're streaming normally: When you call the start() function, the StreamerState.started event is fired first, followed by the BroadcastState.active event after a while.
There may be a small time lapse between started and active, so it's a good idea to show the user that it's in progress with a UI in between.
self.streamer?.enter()
self.streamer?.start()
- Chat Message
-
To send a chat message, use liveChat()?sendMessage() function.
let text = "Hello!"
self.streamer?.liveChat()?.sendMessage(message: text) -
When receiving chat messages, you can do so through the FFLStreamerDelegate described above.
-
The content of an FFLMessage is as follows
field description note origin message type. MEMBER, APP, SYSTEM MEMBER: sending by member, APP: sending by app, SYSTEM: sending by system appUserId user ID appUsername username customType Custom types to distinguish messages If origin is of type SYSTEM, it can be "JOINED", "LEFTED", or "CHANNEL_STAT_UPDATED". message User-sent messages participantCount the number of participants
-
-
func handleMessage(message: FFLMessage) {
switch message.origin {
case .app:
print("message sent by app")
case .members:
print("message sent by member")
case .system:
switch message.customType {
case "JOINED":
print("a user joined")
case "LEAVED":
print("a user left")
case "CHANNEL_STAT_UPDATED":
print("participant count is updated")
default:
break
}
}
}
-
- When you want to end a live broadcast, you call the stop() and exit() functions. Calling stop() stops the live stream. Before you call exit(), you can restart the live via start(). When you call exit(), the live ends and cannot be restarted (if you want to restart it, you'll need to open a new live).
Calling the exit() function notifies the FlipFlop Cloud server that the live has ended. Once the FlipFlop Cloud server has properly closed all live-related information, the StreamerState.closed event will be fired. Therefore, you should wait for the closed event to occur without moving the screen.
This is because if you leave the screen before the closed event occurs, you may not be able to properly notify the server of the live shutdown.
self.streamer?.stop()
self.streamer?.exit()
5. Watching Live
-
As a prelude to live viewing, you will need the following four values The access token can be obtained through the application server as described in section 1.3 above. The remaining items can be obtained through FlipFlop Lite's FlipFlop Cloud - Member Get VideoRooms API. These values are also not provided directly by the SDK, so like the access token, they must be obtained directly through the application server and passed to the client for use.
- access token, video room id, channel id, live url
-
Create a StreamingViewController and create an FFLLivePlayer instance for live viewing. The ACCESS_TOKEN is the token received through the FlipFlop Cloud API, and the VIDEO_ROOM_ID and CHANNEL_ID are the values received from the live list.
class StreamerViewController: UIViewController {
let livePlayer = FlipFlopLite.getLivePlayer(accessToken: ACCESS_TOKEN, videoRoomId: VIDEO_ROOM_ID, channelId: CHANNEL_ID)
} -
Call the prepare() function to initialize the FFLLivePlayer. The prepare() function takes a UIView to view the broadcast screen as the first parameter and a URL to watch live as the second parameter.
class StreamerViewController: UIViewController {
@IBOutlet weak var view: UIView!
let livePlayer = FlipFlopLite.getLivePlayer(accessToken: ACCESS_TOKEN, videoRoomId: VIDEO_ROOM_ID, channelId: CHANNEL_ID)
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
livePlayer.prepare(view: self.view, uri: LIVE_URL))
}
} -
Connecting an FFLLivePlayerDelegate to the StreamingViewController to receive SDK status information
-
When using FFLLivePlayer, you can receive information about its internal state via events. You can use this information to check if the live viewing is going well, etc.
-
The contents of the FFLLivePlayerDelegate are shown below. The thread where the delegate is called may not be the main thread, so you should use DispatchQueue.main to call UI-related parts of the delegate.
public protocol FFLLivePlayerDelegate: AnyObject {
// player state is updated
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didUpdate playerState: PlayerState)
// broadcast state is updated
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didUpdate broadcastState: BroadcastState)
// live url is changed
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didUpdate liveUrl: String)
// channel is opened: can send chat messages after this event
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didOpenChannel channelId: UInt64)
// chat message is received
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didReceive message: FFLMessage)
// some errror happened
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didFail error: FFError)
}-
PlayerState notifies you the state of the FFLLivePlayer.
state description prepared live player is ready to play live started live player is playing live paused live player has been paused stopped live player has been stopped completed live has been finished closed 라이브 시청을 중지했다. -
BroadcastState notifies you the state of your live stream.
state description active notify you that the live is in progress: viewers can see the live inactive notify you that your live has been interrupted: viewers are unable to watch your live
-
-
Connect to the StreamingViewController as shown below.
extension StreamingViewController: FFLLivePlayerDelegate {
// player state is updated
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didUpdate playerState: PlayerState) {
print("[LIVE PLAYER] didUpdatePlayerState: \(playerState)")
}
// broadcast state is updated
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didUpdate broadcastState: BroadcastState) {
print("[LIVE PLAYER] didUpdateBroadcastState: \(broadcastState)")
}
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didUpdate liveUrl: String) {
print("[LivePlayer] didUpdateLiveUrl: \(liveUrl)")
}
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didOpenChannel channelId: UInt64) {
print("[LivePlayer] didOpenChannel: \(channelId)")
}
// chat message is received
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didReceive message: FFLMessage) {
print("[LIVE PLAYER] didReceiveMessage: \(message.message)")
}
// some errror happened
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didFail error: FFError) {
print("[LIVE PLAYER] didFail: \(error.code) / \(error.message)")
}
}
-
-
When you want to start watching live, you call the enter() and start() functions.
livePlayer.enter()
livePlayer.start() -
Sending and Receiving chat messages
-
To send a chat message, use the liveChat()?.sendMessage() function.
let text = "Hello!"
livePlayer.liveChat()?.sendMessage(message: text) -
You can receive chat messages through the events of the FFLLivePlayerDelegate described above.
extension StreamingViewController: FFLLivePlayerDelegate {
// chat message is received
func fflLivePlayer(_ livePlayer: FFLLivePlayer, didReceive message: FFLMessage) {
print("[LIVE PLAYER] didReceiveMessage: \(message.message)")
}
}
-
-
Call the stop() and exit() functions when you want to end the live watch.
livePlayer.stop()
livePlayer.exit()