Skip to content

Instantly share code, notes, and snippets.

@cybrox
Last active March 29, 2019 13:40
Show Gist options
  • Save cybrox/96a487fad05def624c6fcbf57578cb65 to your computer and use it in GitHub Desktop.
Save cybrox/96a487fad05def624c6fcbf57578cb65 to your computer and use it in GitHub Desktop.
Proof-of-concept manual mjpeg-stream swift implementation
// Created by Sven Gehring on 09/01/17.
//
// Licensed under MIT license. Feel free to use in any way
//
// Please note: This is merely a proof of concept that is very
// incomplete overall. Its protocols are not even implement. The
// main purpose was just to figure out how hard and performance-
// intense this manual approach would be.
//
// Spoiler: Just letting WebKit do the work is easier on the battery,
// since it does use gpu for the actual image parsing.
import UIKit
class MjpegStream: NSObject, URLSessionDelegate, URLSessionDataDelegate {
let jpegStartMarker: Data = Data(bytes: [0xff, 0xd8])
let jpegEndMarker: Data = Data(bytes: [0xff, 0xd9])
let maxFramesForImage: Int16 = 100;
var target: UIImageView = UIImageView()
var source: String = ""
var buffer: Data = Data()
var frame: UIImage? = UIImage()
var frameStartIndex: Int = -1;
var frameEndIndex: Int = -1;
var frameLoopCount: Int16 = 0;
var session: URLSession? = nil
var webtask: URLSessionDataTask? = nil
// Initialize a new stream with a given image view as target
//
// - parameter target: The UIImageView to render the output image in
//
// - returns: An MjpegStream instance
init(target: UIImageView) {
super.init()
self.setup(target: target)
}
// Initialize a new stream with a given image view as target and a url string as source
//
// - parameter target: The UIImageView to render the output image in
// - parameter source: The source link for the mjpeg video feed
//
// - returns: An MjpegStream instance
convenience init(target: UIImageView, source: String) {
self.init(target: target)
self.source = source
}
// Start streaming the video from the set source to the set target.
// This method expects `self.source` to be set by either overloading `self.init`
// or manually setting it from the created instance.
//
// - returns: void
func start() -> Void {
guard self.source.characters.count > 10 else {
NSLog("self.source needs to be set before starting the stream!")
return
}
guard self.session != nil else {
NSLog("self.session has not been initialized correctly!")
return
}
let source: URL = URL(string: self.source)!
self.webtask = self.session!.dataTask(with: source)
self.webtask!.resume()
}
// Stops the data task that's pushing data to the target element.
// This method will not discard the set `self.source` or `self.target` element.
//
// - returns: void
func stop() -> Void {
self.webtask?.cancel()
}
// Delegate method implemented in the URLSessionDataDelegate protocol
// Will append the received data to the buffer and call `self.findFrame()` for analyzing the data
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.buffer.append(data)
self.findFrame()
}
// Internal setup routine, will prepare the URLSession for the stream
//
// returns: void
private func setup(target: UIImageView) -> Void {
self.target = target;
self.target.contentMode = .scaleAspectFit;
let defaultConfiguration = URLSessionConfiguration.default
self.session = URLSession(configuration: defaultConfiguration, delegate: self, delegateQueue: nil)
}
// Internal frame finder. Will be called every time the buffer content receives new data
// This method will find jpg frames in the stream and pass them to the renderer
//
// returns: void
private func findFrame() -> Void {
let startRange = self.buffer.range(of: self.jpegStartMarker);
let endRange = self.buffer.range(of: self.jpegEndMarker);
if (startRange != nil) {
self.frameStartIndex = Int(startRange!.lowerBound);
}
if (endRange != nil) {
self.frameEndIndex = Int(endRange!.upperBound);
if (self.frameStartIndex != -1) {
let frameRange: Range = Range(uncheckedBounds: (lower: self.frameStartIndex, upper: self.frameEndIndex))
let restRange: Range = Range(uncheckedBounds: (lower: self.frameEndIndex, upper: self.buffer.count))
self.frame = UIImage(data: self.buffer.subdata(in: frameRange))!;
self.buffer = self.buffer.subdata(in: restRange);
self.renderFrame()
}
}
// NOTE: Prevent buffer from uncontroller growth from invalid data
self.frameLoopCount += 1
if (self.frameLoopCount > self.maxFramesForImage) {
self.buffer = Data()
}
}
// Internal frame renderer. Will render the found jpg frame to the target
// and reset all internal helper properties
//
// returns: void
private func renderFrame() -> Void {
if (self.frame != nil) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.target.image = self.frame
}
}
self.frameStartIndex = -1
self.frameEndIndex = -1
self.frameLoopCount = 0
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment