Last active
March 29, 2019 13:40
-
-
Save cybrox/96a487fad05def624c6fcbf57578cb65 to your computer and use it in GitHub Desktop.
Proof-of-concept manual mjpeg-stream swift implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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