How to upload pre-recorded video files and images

By adding live broadcasting to your app you gain the freedom to stream anything to anyone in realtime. One to many, mobile to mobile. Bambuser's SDK:s takes you there quickly.

Once you're there though, you might find that you sometimes tend to record videos to the camera roll on your phone. Why not provide similar cloud-enabled workflows for those? While at it, why not add support for still images too? For some apps these are valid requirements, while others are better of strictly focused on live-streaming. In case your app belongs to the former category, Bambuser has upload API:s to let you deal with file uploads side by side with live streams.

This guide will look at how to set up a simple upload workflow in a Swift app. For a more in-depth example, see testclient-swift in the iOS SDK bundle and for Android support, see the example app in the Android SDK bundle

This guide is based on Xcode 10.2 and Swift.

Create a new Application

Either build upon the app you created in the broadcasting guide, or:

  • Open Xcode.

  • Select File -> New -> Project...

  • Choose the Single View Application template.

  • Enter a suitable product name, your organization and identifier

  • Ensure Swift is the selected language.

The upload UI

First, let's instantiate Bambuser's file uploader and build a rudimentary upload UI.

In ViewController.swift, instantiate the file uploader and an upload button.

class ViewController: UIViewController, FileUploaderDelegate {
  var fileUploader: FileUploader?

  override func viewDidLoad() {
      super.viewDidLoad()

      fileUploader = FileUploader(_delegate: self)
      // replace with your own applicationId from https://dashboard.bambuser.com/developer
      fileUploader?.applicationId = "GFZalqkR5iyZcIgaolQmA"

      broadcastButton.addTarget(self, action: #selector(self.showImagePicker), for: UIControlEvents.touchUpInside)
      broadcastButton.setTitle("Upload media", for: UIControlState.normal)
      self.view.addSubview(broadcastButton)
    }

    override func viewWillLayoutSubviews() {
      broadcastButton.frame = CGRect(x: 40.0, y: 40.0, width: 100.0, height: 50.0);
    }

The image picker

Next, let's open the image picker: import PhotoKit at the top of the file...

import Photos

...and trigger the image picker in our button callback:

@objc func showImagePicker() {
    let picker = UIImagePickerController()
    picker.delegate = self
    picker.allowsEditing = false
    picker.sourceType = .savedPhotosAlbum
    picker.mediaTypes = [kUTTypeMovie as String]
    picker.videoQuality = .typeIFrame1280x720
    self.present(picker, animated: true, completion: nil)
}

If you want to support images in addition to video, use these media type criteria instead:

picker.mediaTypes = [kUTTypeMovie as String, kUTTypeImage as String]

Next, we need to implement the UIImagePickerControllerDelegate and UINavigationControllerDelegate protocols, to be able to use the picker and do something with the result. Add the following declarations to our ViewController class:

class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  ...

Then implement the following callbacks:

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    picker.dismiss(animated: true, completion: nil);
}

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
    picker.dismiss(animated: true, completion: nil)
    fileUploader?.getTicketAndUploadFromPicker(info: info)
}

If the user picks a file, it is now handed over to our uploader!

If we like to provide some more context, the uploader allows us to set title, author and a custom data string, just like in the broadcasting SDK.

fileUploader?.getTicketAndUploadFromPicker(info: info, title: "atitle", author: "someauthor", customData: "whateveryoulike")

Adding a progress bar

Finally, to ensure the user knows what's going on, let's implement the uploader's FileUploaderDelegate protocol and show an alert with a progress bar while the upload is ongoing:

class ViewController: UIViewController, FileUploaderDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  var fileUploader: FileUploader?
  var uploadDialog: UIAlertController?
  var progressBar: UIProgressView?
}
func uploadStarted() {
    let alert = UIAlertController(title: "Uploading", message: "", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Cancel", style: .default) { (action) in
        self.uploadEnded()
        self.fileUploader?.cancelUpload()
    })
    self.present(alert, animated: true, completion: {
        let pb = UIProgressView(frame: CGRect(x: 30.0, y: 80.0, width: 225.0, height: 90.0))
        pb.progressViewStyle = .bar
        alert.view.addSubview(pb)
        self.progressBar = pb
    })
    uploadDialog = alert
}

func uploadUpdated(_ progress: NSNumber) {
    debugPrint("Upload progress", progress.floatValue)
    progressBar?.setProgress(progress.floatValue, animated: true)
}

func uploadEnded() {
    debugPrint("Upload finished")
    progressBar?.removeFromSuperview()
    uploadDialog?.dismiss(animated: true)
}

func uploadFailed() {
    progressBar?.removeFromSuperview()
    uploadDialog?.dismiss(animated: true)
    showError("", title: "Upload failed")
}

func showError(_ message: String, title: String?) {
    let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    self.present(alert, animated: true)
}

Shortly after a successful upload you should find your media files alongside your broadcasts, both in the content manager and via the REST API. Images are a separate media type while video uploads appear in the same namespace as broadcasts. Broadcasts and uploaded videos can be distinguished by checking whether an uploaded timestamp is present in the broadcast metadata.

FileUploader.swift

The SDK bundle contains an upload helper class in FileUploader.swift. Below is a slightly reduced version that focuses only on UIImagePickerController and PHAsset. If your needs are different, check out the full version of FileUploader.swift in the latest version of the SDK bundle.

Create a new Swift file in your XCode project and paste the following code into it:

/*
 * libbambuser - Bambuser iOS library
 * Copyright 2015 Bambuser AB
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

import Photos

@objc protocol FileUploaderDelegate {
    @objc optional func uploadStarted()
    @objc optional func uploadUpdated(_ progress : NSNumber)
    @objc optional func uploadEnded()
    @objc optional func showError(_ message: String, title: String?)
    @objc optional func uploadFailed()
}

class FileUploader: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
    weak var delegate: AnyObject?
    var applicationId: String?
    var uploadURL: URL?
    var inputFilename: String?
    var ticketTask: URLSessionDataTask?
    var uploadTask: URLSessionUploadTask?
    var filesize: Int64?
    var responseStatus: Int?

    init(_delegate: AnyObject) {
        delegate = _delegate
        uploadURL = nil
        inputFilename = nil
    }

    private func dispatchError(_ errorMessage: String) {
        DispatchQueue.main.async(execute: {
            debugPrint("FileUploader error: \(errorMessage)")
            self.delegate?.showError?(errorMessage, title: nil)
        })
    }

    func getTicketAndUploadFromPicker(info: [UIImagePickerController.InfoKey: Any], title: String? = nil, author: String? = nil, customData: String? = nil) {
        guard let filePath = (info[UIImagePickerController.InfoKey.mediaURL] as? URL)?.path else {
            return self.dispatchError("Media not found")
        }

        // Ticket request payload
        var params:[String:String] = [String:String]()

        // Custom metadata
        if (title != nil) {
            params["title"] = title
        }
        if (author != nil) {
            params["author"] = author
        }
        if (customData != nil) {
            params["custom_data"] = customData
        }

        // Media metadata
        params["type"] = info[UIImagePickerController.InfoKey.mediaType] as! String == "public.image" ? "image" : "video"
        params["filename"] = params["type"]! + "." + (params["type"] == "image" ? ".jpg" : ".mp4")
        let asset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset
        if (asset != nil) {
            let date = asset?.creationDate?.timeIntervalSince1970
            if (date != nil) {
                params["created"] = String(Int(date!))
            }
        }

        // Client metadata
        var name: [Int32] = [CTL_HW, HW_MACHINE]
        var size: Int = 2
        sysctl(&name, 2, nil, &size, nil, 0)
        var hw_machine = [CChar](repeating: 0, count: Int(size))
        sysctl(&name, 2, &hw_machine, &size, nil, 0)
        let hardware: String = String(cString: hw_machine)
        params["device_model"] = hardware
        params["platform"] = "iOS"
        params["platform_version"] = UIDevice.current.systemVersion
        params["manufacturer"] = "Apple"

        let postData = try? JSONSerialization.data(withJSONObject: params, options: JSONSerialization.WritingOptions.prettyPrinted)
        let request = NSMutableURLRequest()
        request.url = URL(string: "https://cdn.bambuser.net/uploadTickets")
        request.httpMethod = "POST"
        request.timeoutInterval = 10.0
        request.setValue(postData?.count.description, forHTTPHeaderField: "Content-Length")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/vnd.bambuser.cdn.v1+json", forHTTPHeaderField: "Accept")
        request.setValue(applicationId, forHTTPHeaderField: "X-Bambuser-ApplicationId")
        request.setValue(Bundle.main.bundleIdentifier! + " " + (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String), forHTTPHeaderField: "X-Bambuser-ClientVersion")
        request.setValue("iOS " + UIDevice.current.systemVersion, forHTTPHeaderField: "X-Bambuser-ClientPlatform")
        request.httpBody = postData

        ticketTask = Foundation.URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) in
            guard error == nil else { return self.dispatchError(error!.localizedDescription) }
            do {
                try JSONSerialization.jsonObject(with: data!, options: .mutableContainers)
            } catch {
                return self.dispatchError(NSString(data: data!, encoding:String.Encoding.utf8.rawValue)! as String)
            }
            let parsedData = (try! JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions())) as! NSDictionary
            guard parsedData["error"] == nil else {
                let responseError = parsedData["error"] as! NSDictionary
                print(responseError)
                return self.dispatchError(responseError["message"]! as! String)
            }
            self.uploadURL = URL(string: (parsedData["upload_url"] as? String)!)
            guard self.uploadURL != nil else {
                return self.dispatchError("Unexpected response from upload server, please try again.")
            }
            DispatchQueue.main.async(execute: {
                self.uploadFile(filePath)
            })
        })
        ticketTask?.resume()
    }

    func uploadFile(_ filename: String) {
        inputFilename = filename
        let request = NSMutableURLRequest()
        request.url = uploadURL!
        request.httpMethod = "PUT"
        var attr = (try? FileManager.default.attributesOfItem(atPath: inputFilename!)) as [FileAttributeKey : AnyObject]?
        filesize = attr?[FileAttributeKey.size]?.int64Value
        responseStatus = 0

        request.addValue(filesize!.description, forHTTPHeaderField: "Content-Length")

        let inputStream = InputStream(fileAtPath: inputFilename!)

        request.httpBodyStream = inputStream
        request.timeoutInterval = 60.0

        let sessionConfig:URLSessionConfiguration = URLSessionConfiguration.default
        let queue = OperationQueue()
        let session:Foundation.URLSession = Foundation.URLSession(configuration: sessionConfig, delegate: self, delegateQueue: queue)
        uploadTask = session.uploadTask(withStreamedRequest: request as URLRequest)
        uploadTask?.resume()
        DispatchQueue.main.async(execute: {
            self.delegate?.uploadStarted?()
        })
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) {
        let inputStream = InputStream(fileAtPath: inputFilename!)
        completionHandler(inputStream)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        let progress = Float(totalBytesSent) / Float(filesize!)
        DispatchQueue.main.async(execute: {
            self.delegate?.uploadUpdated?(NSNumber(value: progress))
        })
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        DispatchQueue.main.async(execute: {
            if (error != nil) {
                if (error!._code != NSURLErrorCancelled) {
                    self.delegate?.uploadFailed?()
                }
            } else {
                if (self.responseStatus! == 200) {
                    self.delegate?.uploadEnded?()
                } else {
                    self.delegate?.uploadFailed?()
                }
            }
        })
        self.cancelAndFinalizeUploadTask()
    }

    func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
        if (error != nil) {
            DispatchQueue.main.async(execute: {
                self.delegate?.uploadFailed?()
            })
        }
        self.cancelAndFinalizeUploadTask()
    }

    func urlSession(_ session: Foundation.URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        print(NSString(data: data, encoding: String.Encoding.utf8.rawValue)!)
    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {

        if let httpResponse = response as? HTTPURLResponse {
            responseStatus = httpResponse.statusCode

            if (responseStatus == 200) {
            } else if (responseStatus! >= 400 && responseStatus! < 500) {
                // Client error
                print("Upload was not accepted");
            } else if (responseStatus! >= 500 && responseStatus! < 600) {
                // Server error
                print("Upload was not accepted");
            } else {
                print("Unexpected status code:", responseStatus!);
            }
        }
        completionHandler(Foundation.URLSession.ResponseDisposition.allow)
    }

    func cancelUpload() {
        self.cancelAndFinalizeUploadTask()
    }

    fileprivate func cancelAndFinalizeUploadTask() {
        ticketTask?.cancel()
        uploadTask?.cancel()
        ticketTask = nil
        uploadTask = nil
    }
}