0

I'm running a mjpeg streamer and trying to set the stream as a texture in three.js.

I'm doing this by writing it to a canvas and then the canvas as the texture. However, I get a tainted canvas error.

DOMException: Failed to execute 'texImage2D' on 'WebGLRenderingContext': 
Tainted canvases may not be loaded.

I have added the crossOrigin header on the server, and it is recieved as:

  Access-Control-Allow-Origin:*
  Content-type:multipart/x-mixed-replace; boundary=--jpgboundary
  Date:Tue, 04 Apr 2017 22:27:35 GMT
  Server:BaseHTTP/0.3 Python/2.7.9**strong text**

on the client side I have added the appropriate crossOrigin attributes

<p>Image to use:</p>
<img id="stream" crossorigin="anonymous" 
src="http://192.168.1.224:8080/cam.mjpg">


<p>Canvas:</p>
<canvas id="myCanvas" width="200" height="200" style="border:1px solid 
#d3d3d3;">
Your browser does not support the HTML5 canvas tag.
</canvas>

function updateCanvas() {
    var c = document.getElementById("myCanvas");
    var ctx2 = c.getContext("2d");
    var img = document.getElementById("stream");
    img.crossOrigin = "anonymous";
    ctx2.drawImage(img, 0, 0);
}

Does anyone have any idea what isn't working here?

Edit: sorry i should have clarified, putting it on the canvas works fine, the problem is that when i try and use that canvas as a texture, then the canvas is tainted and it fails

function init() {
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height);
    document.body.appendChild(renderer.domElement);

    scene = new THREE.Scene();

    camera = new THREE.PerspectiveCamera(70, width / height, 1, 1000);
    camera.position.z = 500;
    scene.add(camera);
    window.setTimeout("updateCanvas()", 1000); //start with a delay
    texture = new THREE.Texture(canvas);
    var material = new THREE.MeshBasicMaterial({ map: texture });
    geometry = new THREE.BoxGeometry( 200, 200, 200 );
    mesh = new THREE.Mesh( geometry, material );
    scene.add( mesh );
    canvas.width = canvas.height = size;
    renderer.setClearColor(0xeeeeee, 1);
}


function animate() {
   requestAnimationFrame(animate);
   updateCanvas();
   texture.needsUpdate = true;
   mesh.rotation.y += 0.01;
   renderer.render(scene, camera);
}
  • Am I not setting the crossOrigin attribute before the src? I am also calling updateCanvas repeatedly in my code later on, to update the next frame. – simon scott Apr 05 '17 at 12:51
  • @gman , do you know if `gl.texImage2D` has the same [limitations](https://html.spec.whatwg.org/multipage/scripting.html#image-sources-for-2d-rendering-contexts:canvasimagesource-3) on animated media in `` as `context2d.drawImage()` ? – Kaiido Apr 05 '17 at 13:18
  • `texImage2D` is stricter than `drawImage`. `drawImage` can take a cross domain image without CORS permission. Doing so will mark the canvas as dirty. `texImage2D` will not take a cross domain image without permission. a WebGL canvas can never be *dirty* because you can't use cross domain images without CORS permissions period. – gman Apr 05 '17 at 13:24
  • @gman, sorry I did split the discussion with a comment on your answer. If the 2d canvas used for the texture is tainted before then texImage2d won't work right? Then it is probably OP's issue. But my question was Does he even need to pass through a 2d canvas? Can texImage2d handle animated images, unlike drawImage? – Kaiido Apr 05 '17 at 14:16
  • Correct, passing a cross domain non CORS permission video through a canvas won't work. Passing through a canvas does nothing AFAIK except maybe let you scale the video before uploading to a texture. You can use [a video tag directly with texImage2D](http://webglsamples.org/video/video.html). Though just recently there's [a bug in Chrome currently being fixed](https://bugs.chromium.org/p/chromium/issues/detail?id=701060) – gman Apr 05 '17 at 14:29
  • To OP and gman, my bad, I somehow missed the attribute in the html markup... Then I am clue-less since the image should never be drawn if the request was not served correctly by the server. – Kaiido Apr 05 '17 at 14:44
  • @gman ok for video tag, you can draw it with drawImage too, the restriction I am talking about is only on animated media in `` tag (gif mjpeg smil animated svg) – Kaiido Apr 05 '17 at 14:47
  • AFAICT neither WebGL nor Canvas2D support animated images. [See this answer](http://stackoverflow.com/questions/9276594/how-to-play-gif-inside-canvas-in-html5) – gman Apr 05 '17 at 15:49
  • The canvas is working fine though? its when I use the canvas as a texture thats broken – simon scott Apr 05 '17 at 15:58
  • Canvas2D allows cross origin images without CORS permission. The canvas just gets marked as "dirty" and both `canvas.toDataURL` and `ctx.getImageData` stop working. WebGL does not allow cross origin images without CORS permission period, that includes not allowing using a dirty canvas. So again that suggests you're not actually getting CORS permission to use the image. How about trying `https://i.imgur.com/TSiyiJv.jpg` as your URL and see if the error message goes away (or changes) – gman Apr 06 '17 at 04:39
  • @gman If the server doesn't send the correct headers on the image, the image with a crossOrigin attribute won't load period. One thing to consider though would be a bug in browsers. OP is talking about a motion jpeg stream. It is possible that while the first frame has the correct CORS permission, subsequent frames in the stream won't have anymore. But this would mean that OP is relying on a chrome bug to start of. Canvas2D should draw only the first frame of the stream. – Kaiido Apr 09 '17 at 03:30
  • @gman, All modern browsers do support motion jpeg... You just have to pass a stream url to an img element... http://webcam.st-malo.com/axis-cgi/mjpg/video.cgi?resolution=704x576&dummy=1491717369754 – Kaiido Apr 09 '17 at 05:57

2 Answers2

0

I don't see any reason your code won't work. Setting img.crossOrigin has no point because the image has already been downloaded in your example.

Trying it with an image from imgur it works. Which suggests the issue is with your server?

function updateCanvas() {
  var c = document.getElementById("myCanvas");
  var ctx2 = c.getContext("2d");
  var img = document.getElementById("stream");
  ctx2.drawImage(img, 0, 0);
  try {
    const d = c.toDataURL();
    log("canvas is not dirty, CORS permission received, it can be used in WebGL");
  } catch (e) {
    log("canvas IS dirty, no CORS permission, it can *NOT* be used in WebGL");
  }
}

function log(...args) {
  const elem = document.createElement("pre");
  elem.textContent = [...args].join("\n");
  document.body.appendChild(elem);
}


document.getElementById("stream").addEventListener('click', updateCanvas);
img, canvas { width: 32px; }
p { margin: 0; }
<p>Click the Image:</p>
<img id="stream" crossorigin="anonymous" src="https://i.imgur.com/TSiyiJv.jpg">


<p>Canvas:</p>
<canvas id="myCanvas" width="200" height="200" style="border:1px solid 
#d3d3d3;">
</canvas>

Maybe check the headers from imgur and see what's different?

gman
  • 100,619
  • 31
  • 269
  • 393
0

This is a chrome bug :

I had to set up an ffserver + mjpeg proxy on my localhost to confirm this, but, chrome doesn't keep the crossOrigin headers on requests made on your motion jpeg stream...

Firefox doesn't have this bug, and I didn't found a solid front-end workaround.

Actually the only workaround I have found is to use a proxy, which is not really easy with mjpeg streams, but this one based on express-js does work pretty well.

But also, note that browsers should only paint the first frame of an mjpeg stream on the canvas [specs], chrome also have a bug here, don't rely on this since it may fail any time soon, when they'll fix it.


Edit

webgl's texImage2d doesn't seem to have the same limitations about animated images as drawImage has.

So for your case, don't even pass through a 2D canvas. You can directly use the mjpeg stream inside an <img> tag and use it as an updating texture.
(Though you'd still have to use a proxy for chrome)

Community
  • 1
  • 1
Kaiido
  • 123,334
  • 13
  • 219
  • 285