Search code examples
iosswiftswiftuistreamingmjpeg

SwiftUI - MJPEG Video stream not updating Image in View


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.


Solution

  • 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.