⚠️ Sunset Notice: This service will be discontinued as of September 30th, 2023. Learn more »
Did you come here for Live Video Shopping?
This is documentation for Bambuser Live Streaming SDK.
If you're looking for documentation regarding Live Video Shopping (opens new window) , see these pages (opens new window)
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 (opens new window) 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 (opens new window) and for Android
support, see the example app in the Android SDK bundle (opens new window)
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 (opens new window) 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 (opens new window)
and via the REST API (opens new window).
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 (opens new window)
and PHAsset (opens new window).
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
}
}