Given the following StreamView()
:
struct StreamView: View {
@StateObject var stream = MJPEGStream()
var body: some View {
MpegView(mjpegStream: self.stream)
.background(.red)
.frame(width: 200, height: 200)
}
}
struct StreamView_Previews: PreviewProvider {
static var previews: some View {
StreamView()
}
}
I have the following MpegView()
that implements ObservableObject
:
class MJPEGStream: ObservableObject {
@Published var stream = MJPEGStreamLib()
init() {
self.stream.play(url: URL(string: "http://192.168.1.120/mjpeg/1")!)
}
}
struct MpegView: View {
@ObservedObject var mjpegStream: MJPEGStream
var body: some View {
Image(uiImage: self.mjpegStream.stream.image)
.resizable()
}
}
Basically the following class replaces an instance of var image = UIImage()
with an updated image of the MJPEG stream:
class MJPEGStreamLib: NSObject, URLSessionDataDelegate {
enum StreamStatus {
case stop
case loading
case play
}
var receivedData: NSMutableData?
var dataTask: URLSessionDataTask?
var session: Foundation.URLSession!
var status: StreamStatus = .stop
var authenticationHandler: ((URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
var didStartLoading: (() -> Void)?
var didFinishLoading: (() -> Void)?
var contentURL: URL?
var image = UIImage()
override init() {
super.init()
self.session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
}
convenience init(contentURL: URL) {
self.init()
self.contentURL = contentURL
self.play()
}
deinit {
self.dataTask?.cancel()
}
// Play function with url parameter
func play(url: URL) {
// Checking the status for it is already playing or not
if self.status == .play || self.status == .loading {
self.stop()
}
self.contentURL = url
self.play()
}
// Play function without URL paremeter
func play() {
guard let url = self.contentURL, self.status == .stop else {
return
}
self.status = .loading
DispatchQueue.main.async {
self.didStartLoading?()
}
self.receivedData = NSMutableData()
let request = URLRequest(url: url)
self.dataTask = self.session.dataTask(with: request)
self.dataTask?.resume()
}
// Stop the stream function
func stop() {
self.status = .stop
self.dataTask?.cancel()
}
// NSURLSessionDataDelegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
// Controlling the imageData is not nil
if let imageData = self.receivedData, imageData.length > 0,
let receivedImage = UIImage(data: imageData as Data) {
if self.status == .loading {
self.status = .play
DispatchQueue.main.async {
self.didFinishLoading?()
}
}
// Set the imageview as received stream
DispatchQueue.main.async {
self.image = receivedImage
}
}
self.receivedData = NSMutableData()
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.receivedData?.append(data)
}
// NSURLSessionTaskDelegate
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
var credential: URLCredential?
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
// Getting the authentication if stream asks it
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
if let trust = challenge.protectionSpace.serverTrust {
credential = URLCredential(trust: trust)
disposition = .useCredential
}
} else if let onAuthentication = self.authenticationHandler {
(disposition, credential) = onAuthentication(challenge)
}
completionHandler(disposition, credential)
}
}
Then in my main ContentView()
I simply have:
struct ContentView: View {
var body: some View {
StreamView()
}
}
The problem is that the Image
in the MpegView()
is not getting updated with the received frames from the stream. I'm not sure if it's my implementation for the class library or the @Published
or @StateObject
properties.
NOTE: I can confirm that the stream works via the web browser and also if I debug what the receivedImage
is it's the actual frame from the streamed video.
The value of your observed property stream
in MJPEGStream
is a pointer to an MJPEGStreamLib
object.
The only time that property changes, and the only time your ObservableObject
will cause the MpegView
to be updated, is when you first assign a value to the pointer - when the MpegView
is first created. After that, the pointer to the object never changes, even if the object it points to is quickly generating images. So your view never updates.
If you want your Swift view to update whenever the image in your MJPEGStreamLib
object changes, then you need to make MJPEGStreamLib
the ObservableObject
and mark its image
property as @Published
.