motion-canvas
motion-canvas copied to clipboard
Rendering fails when using remote pictures
Describe the bug If a scene has images from remote source, trying to render will result in an error:
Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
To Reproduce Have a scene with a remote Image, then try to render.
import { makeScene2D } from "@motion-canvas/2d";
import { Image, Layout } from "@motion-canvas/2d/lib/components";
import { waitFor } from "@motion-canvas/core/lib/flow";
export default makeScene2D(function* (scene) {
scene.add(<Layout>
<Image scale={.2} src={"https://images.unsplash.com/photo-1515229144611-617d3ce8e108"}/> {/* any remote picture works */}
</Layout>)
yield* waitFor(1)
});
Additional context Error occurs both on Firefox and Chromium.
Kinda similar to #234 Not sure if we can fix this since it seems like a CORS issue on the image server side
What might work is defining a proxy in vite.config.ts
that does all requests on behalf of the Browser.
I would add a new helper utility that wraps a src string, like https://via.placeholder.com/300.png/09f/fff
into a base64, escaped string that gets passed to a proxy endpoint on vite, say /proxy
.
const originalSrc: string;
return `/proxy/${encodeURIComponent(originalSrc)}`
I will have to look into how proxying works with vite. A quick glance at the docs tells me I need to specify a server, which would not work in this use case as we would need to define all endpoints.
Proof of Concept works
Click to expand / Outdated: I basically implemented a Plugin for Vite which adds some a Middleware to Vite's Server that intercepts any calls starting with `/proxy/`.
const proxyPlugin = (): Plugin => {
return {
name: "proxy-server",
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (!req.url || !req.url.startsWith("/proxy/")) {
return next()
}
if(req.method !== "GET") {
const msg = "Only GET requests are allowed"
res.writeHead(400, msg)
res.write(msg)
res.end()
return;
}
// Extract destination, e.g
function extractDestinationUrl(url: string) {
try {
const withoutPrefix = url.replace("/proxy/", "")
const asUrl = new URL(decodeURIComponent(withoutPrefix))
console.log(`"${asUrl.protocol}"`)
if(asUrl.protocol !== "http:" && asUrl.protocol !== "https:") {
throw new Error("Only supported protocols are http and https") // TODO: Is this enough to prevent access to file:// ?
}
return [asUrl, undefined] as const
}
catch(err) {
return [undefined, err+""] as const
}
}
const [sourceUrl, error] = extractDestinationUrl(req.url)
if (error || !sourceUrl) {
res.writeHead(400, error)
res.write(error)
res.end()
return
}
// Call URL on behalf of callee
const proxy_req = http.request( {
hostname: sourceUrl.hostname,
path: sourceUrl.pathname,
method: "get"
}, (proxy_res) => {
let buffer: any[] = []
console.log("ping")
proxy_res.on("data", (chunk) => {
buffer.push(chunk )
})
proxy_res.on("error", () => {
res.statusCode = 400
res.statusMessage = "Proxying failed"
res.end()
})
proxy_res.on("close", () => {
const body = Buffer.concat(buffer)
// only proxy headers we actually need
const allowedHeaders = [
"content-type", "content-length", "cache-control"
]
for(const header of allowedHeaders) {
const headerValue = proxy_res.headers[header]
if(!headerValue) {
continue // Header does not exist. Ignore
}
res.setHeader(header, headerValue)
}
// Additionally set the X-Proxied-Url Header for debugging
res.setHeader("X-Proxied-Url", sourceUrl.toString())
res.end(body)
console.log("done")
})
}).end()
})
}
}
}
Yesterday I took the time to create a Plugin that provides a proxy server, and a utility function to wrap remote requests, so that they get sent to the proxy instead. It's basically what I shown in the Proof of Concept, with more checks, configuration and using Axios instead of raw http
https://github.com/WaldemarLehner/motion-canvas-cors-proxy
@Ross-Esmond mentioned it might be a good idea to bring this into the core repo instead.
Relevant discussion on Discord: https://discord.com/channels/1071029581009657896/1074755895529062531/1075236819597267044
The image in the example should work, since it's served with the "Access-Control-Allow-Origin: *" header. I added image.crossOrigin = 'anonymous'
to the Image component and rendering works as normal. This won't work for images without the Access-Control-Allow-Origin header, though.