Skip to main content

Tutorial

1. Prerequisites

  1. Requirements

    • iOS 14 or higher
    • Xcode 13.4.1 or higher
    • Swift 5.6.1 or higher
  2. Creating Application

    1. 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.
  3. Getting access token from server

    1. 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
    2. For more information on using the API, refer the FlipFlop Cloud - Member Login API documentation.
    3. For more information about access tokens, see the Authentication section of Core Concepts

2. Installing SDK

  1. 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
  2. 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
  3. 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

  1. 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)
}
  1. 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
    KeyDescriptiondefault value
    presetvideo width와 heighthd1280x720
    videoBitratevideo bitrate2000 * 1024
    keyFrameIntervalkey frame interval2
    fpsframerate30
    sampleRateaudio samplerate48000
    audioBitrateaudio bitrate64 * 1024
    cameraPoscamera 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)
}

}
  1. Setting Live Title
    1. 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)
  1. Connecting an FFLStreamerDelegate to a StreamingController to receive SDK status information

    1. 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.
    2. 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

      statedescription
      preparednotify you that you're ready to go live.
      startednotify you that your live stream has started. This status does not mean that viewers can see you live.
      stoppednotify you that the live stream has been interrupted.
      closednotify you that the live has ended.
    • BroadcastState: Live State

      statedescription
      activenotify you that the live is in progress (viewers can see the live).
      inactivenotify 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.

      statedescription
      normallive streaming is ok
      alert_1the state of live streaming is slightly poor
      alert_2live state is worse than alert_1
      alert_3live state is worse than alert_2
    1. 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)")
    }
    }
  2. 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()
  1. Chat Message
    1. To send a chat message, use liveChat()?sendMessage() function.

      let text = "Hello!"
      self.streamer?.liveChat()?.sendMessage(message: text)
    2. When receiving chat messages, you can do so through the FFLStreamerDelegate described above.

      1. The content of an FFLMessage is as follows

        fielddescriptionnote
        originmessage type. MEMBER, APP, SYSTEMMEMBER: sending by member, APP: sending by app, SYSTEM: sending by system
        appUserIduser ID
        appUsernameusername
        customTypeCustom types to distinguish messagesIf origin is of type SYSTEM, it can be "JOINED", "LEFTED", or "CHANNEL_STAT_UPDATED".
        messageUser-sent messages
        participantCountthe number of participants
    3. 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
      }
      }
      }
  2. 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

  1. 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.

    1. access token, video room id, channel id, live url
  2. 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)
    }
  3. 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))
    }

    }
  4. Connecting an FFLLivePlayerDelegate to the StreamingViewController to receive SDK status information

    1. 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.

    2. 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)
      }
      1. PlayerState notifies you the state of the FFLLivePlayer.

        statedescription
        preparedlive player is ready to play live
        startedlive player is playing live
        pausedlive player has been paused
        stoppedlive player has been stopped
        completedlive has been finished
        closed라이브 시청을 중지했다.
      2. BroadcastState notifies you the state of your live stream.

        statedescription
        activenotify you that the live is in progress: viewers can see the live
        inactivenotify you that your live has been interrupted: viewers are unable to watch your live
    3. 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)")
      }
      }
  5. When you want to start watching live, you call the enter() and start() functions.

    livePlayer.enter()
    livePlayer.start()
  6. Sending and Receiving chat messages

    1. To send a chat message, use the liveChat()?.sendMessage() function.

      let text = "Hello!"
      livePlayer.liveChat()?.sendMessage(message: text)
    2. 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)")
      }
      }
  7. Call the stop() and exit() functions when you want to end the live watch.

    livePlayer.stop()
    livePlayer.exit()