views:

520

answers:

4

I have an IP Camera that streams out live video to a web site of mine. Problem is, it is powered by an ActiveX control. Even worse, this control is unsigned. To provide a more secure alternative to the people that are using browsers other than IE, or are (rightfully) unwilling to change their security settings, I am tapping into the cameras built in snap-shot script that serves up a 640x480 live JPEG image. The plan was to update the image live on the screen every ~500ms using Javascript without having to reload the entire page.

I tried using the Image() object to pre-load the image and update the SRC attribute of the image element when onload fired:

function updateCam() {
  var url = "../snapshot.cgi?t=" + new Date().getTime();
  img = new Image();
  img.onload = function() {
    $("#livePhoto").attr("src", url);
    camTimer = setTimeout(updateCam, 500);
  }
  img.src = url;
}

This worked decently, but it was difficult to determine when the camera had been disabled, which I needed to do in order to degrade gracefully. The internal snapshot script is setup to return an HTTP status code of 204 (No Content) under this circumstance, and of course there is no way for me to detect that using the Image object. Additionally, the onload event was not 100% reliable.

Therefore, I am using the jQuery (version 1.2.6) ajax function to do a GET request on the URL, and on the complete callback I evaluate the status code and set the URL accordingly:

function updateCam() {
  var url = "../snapshot.cgi?t=" + new Date().getTime();

  $.ajax({
    type: "GET",
    url: url,
    timeout: 2000,
    complete: function(xhr) {
      try {
        var src = (xhr.status == 200) ? url : '../i/cam-oos.jpg';
        $("#livePhoto").attr("src", src);            
      }
      catch(e) {
        JoshError.log(e);
      }

      camTimer = setTimeout(updateCam, 500);
    }
  });
}

And this works beautifully. But only in IE! This is the question that I would like to have answered: Why doesn't this work in Firefox or Chrome? The complete event does not even fire in Firefox. It does fire in Chrome, but only very rarely does setting the SRC actually load the image that was requested (usually it displays nothing).

A: 

I believe the jquery will try to interpret the response from the server. I believe some browsers are more tolerant of the response interpretation so more restrictive browsers will fail because an image cannot be seen as HTML!

The solution to this would be to use a HEAD request type instead of a GET ( type: "HEAD" ). That way you will still get status responses back, without grabbing the image itself. Therefore the response will be empty (and shouldn't mess you up). Also, the response should be much faster.

Kazar
Using HEAD gives me a much faster response, but it's a 404!
Josh Stodola
Is the webcam script a custom HTTP implementation?
Kazar
No, it's built into the internal server of the IP Cam and I have no way of modifying it. I could write my own proxy but that's not going to solve anything.
Josh Stodola
A: 

Posting a second answer, because the first was just really incorrect. I can't test this solution (because I don't have access to your webcam script), but I would suggest trying to sanitise the response from the camera - since you obviously can't handle the raw image data, try adding the dataFilter setting like so:

function updateCam() {
  var url = "../snapshopt.cgi?t=" + new Date().getTime();

  $.ajax({
    type: "GET",
    url: url,
    timeout: 2000,
    dataFilter : function(data, type) {
       return '<div></div>' //so it returns something...
    },
    complete: function(xhr) {
      try {
        var src = (xhr.status == 200) ? url : '../i/cam-oos.jpg';
        $("#live").attr("src", src);            
      }
      catch(e) {
        JoshError.log(e);
      }

      camTimer = setTimeout(updateCam, 500);
    }
  });
}

Like I said, I haven't been able to test this - but it might allow jquery to use the status codes without breaking like crazy.

Kazar
No luck :(Everything is working perfectly in Internet Explorer. I might try writing a proxy to wrap the CGI script and return the image as base-64 encoded within XML. What a PITA.
Josh Stodola
Indeed, what a pain, afraid I'm out of ideas then - obviously there could be an underlying issue that is nothing to do with the javascript. Probably not helped by the fact that the webcam doesn't implement full HTTP (rolls eyes).
Kazar
Not sure what you mean by implementing full HTTP. It implements it fine. Regardless, I could very easily write a proxy to return whatever HTTP I desired. Unfortunately, that still does not resolve the issue.
Josh Stodola
A: 
img.onerror = function(){
    alert('offline');
}
epascarello
Thanks Eric. I really need to eval the status code though because it could also be a 304. I need to react to these differently. Plus, as mentioned, the onload event is unreliable. It doesn't always fire. I didn't want to do a setInterval so as to make the update frequency depend on connection speed, so I do a timeout inside load event (which calls the callee). If onload never fires for whatever reason, the updating stops. Can you see any reason why my AJAX approach would fail in Firefox?
Josh Stodola
Would something like adding another QS value force it to fetch it. Maybe it sees it as cached since you called it with the Ajax call?var src = (xhr.status == 200) ? url + "
epascarello
My bad, I mis-interpreted your comment. I would try that but the "complete" callback is not fired AT ALL in Firefox. Not even once. Even it was possible to try, doesn't that eliminate the whole point of the pre-loading? I'd just set the SRC of the image tag directly if I didn't have the need to pre-load. Thanks again for helping me out! I am so stumped it's not even funny.
Josh Stodola
A: 

Well, I ended up using the data URI scheme (hat tip to Eric Pascarello) for non-IE browsers. I wrote a HTTP handler (in VB.NET) to proxy the IP camera and base-64 encode the image:

Imports Common
Imports System.IO
Imports System.Net

Public Class LiveCam
    Implements IHttpHandler

    Private ReadOnly URL As String = "http://12.34.56.78/snapshot.cgi"
    Private ReadOnly FAIL As String = Common.MapPath("~/i/cam-oos.jpg")        

    Public Sub ProcessRequest(ByVal context As System.Web.HttpContext) Implements System.Web.IHttpHandler.ProcessRequest
        Dim Data As Byte()

        With context.Response
           .ContentEncoding = Encoding.UTF8
           .ContentType = "text/plain"
           .Write("data:image/png;base64,")

           Try
               Using Client As New WebClient()
                   Data = Client.DownloadData(URL)
               End Using
           Catch ex As WebException
               Data = File.ReadAllBytes(FAIL)
           End Try

           .Write(Convert.ToBase64String(Data))
        End With
    End Sub
End Class

Then I just put a little non-IE detection (using the classic document.all check) in order to call the correct URL/set the correct SRC:

function updateCam() {
  var url = (document.all) ? "../snapshot.cgi?t=" : "../cam.axd?t=";

  url += new Date().getTime();

  $.ajax({
    type: "GET",
    url: url,
    timeout: 2000,
    complete: function(xhr) {
      try {
        var src;

        if(document.all)
          src = (xhr.status == 200) ? url : '../i/cam-oos.jpg';
        else
          src = xhr.responseText;

        $("#livePhoto").attr("src", src);            
      }
      catch(e) {
        JoshError.log(e);
      }

      camTimer = setTimeout(updateCam, 500);
    }
  });
}

It's very unfortunate I had to resort to this workaround for. I hate browser detection code, and I hate the additional load that is put on my server. The proxy will not only force me to waste more bandwidth, but it will not operate as efficiently because of the inherent proxy drawbacks and due to the time required to base-64 encode the image. Additionally, it is not setup to degrade as gracefully as IE. Although I could re-write the proxy to use HttpWebRequest and return the proper status codes, etc. I just wanted the easiest way out as possible because I am sick of dealing with this!

Thanks to all!

Josh Stodola