120 lines
4.7 KiB
Clojure
120 lines
4.7 KiB
Clojure
(ns heyarne.vanilla-sky.tiptaps)
|
|
|
|
;; this file contains the first steps.
|
|
;; we want to analyze pictures of cctv cameras. the thing we start with is
|
|
;; finding those pictures and loading them so we can manipulate them.
|
|
|
|
;; for some reason the generic example (using java.net.URL.) from the enlive
|
|
;; tutorial does not work, the pages return a 403 Forbidden, which is why we use
|
|
;; clj-http and parse the body directly:
|
|
|
|
(require '[clj-http.client :as http])
|
|
(require '[net.cgrand.enlive-html :as html])
|
|
|
|
(defn fetch-url [url]
|
|
(future (html/html-snippet (:body (http/get url)))))
|
|
|
|
(def some-detail-page
|
|
@(fetch-url "https://www.insecam.org/en/view/540433/"))
|
|
|
|
(def some-camera-image
|
|
(->
|
|
(html/select some-detail-page [:#image0])
|
|
(first)
|
|
(html/attr-values :src)
|
|
(first)))
|
|
|
|
;; cool! so we have the url of a camera image we chose randomly by fair dice
|
|
;; roll. we'll eventually have to think of a way to get a good camera image
|
|
;; dynamically but we can save that for later. for now let's load the
|
|
;; image and see what we can do with it.
|
|
|
|
;; with the image we encountered above, server-side push is implemented
|
|
;; using the multipart/mixed-replace header. this means that essentially the
|
|
;; connection is kept open and as soon as a complete chunk of data is received,
|
|
;; a browser would be replacing the currently displayed image with the new one.
|
|
;; we're only interested in the first chunk of data, so we need to figure out
|
|
;; how we can close the connection afterwards and discard the other ones.
|
|
|
|
;; TODO: To read the image the following is done
|
|
;; - Find the first boundary
|
|
;; - Load all following bytes into a buffer until the buffer appears the next time
|
|
;; - Return the buffer
|
|
|
|
;; NOTE A more elegant method might be this:
|
|
;; - Convert the stream into a lazy sequence of bytes
|
|
;; - Partition the lazy sequence whenever you find (str "--" boundary)
|
|
;; - Select the part of the sequence you want
|
|
|
|
(defn input->byte-seq [input]
|
|
(lazy-seq (let [b (.read input)]
|
|
;; -1 marks the end of the stream
|
|
(when (not= b -1)
|
|
(cons b (input->byte-seq input))))))
|
|
|
|
(comment
|
|
;; Let's test this
|
|
(input->byte-seq (java.io.StringReader. "Hello World")))
|
|
|
|
;; This is a helper function we need later.
|
|
|
|
(defn find-index
|
|
"Returns the index of the first occurence of `el` in `coll` or `nil` if it's
|
|
not found."
|
|
[el coll]
|
|
(first (keep-indexed #(when (= el %2) %1) coll)))
|
|
|
|
(defn partition-with-seq
|
|
"Partitions `coll` every time `sep` appears. The last item returned is
|
|
everything that follows after the last time `sep` was found"
|
|
[sep coll]
|
|
(lazy-seq
|
|
(when (seq coll)
|
|
(let [idx (find-index sep (partition (count sep) 1 coll))]
|
|
(if idx
|
|
(cons (take idx coll) (partition-with-seq sep (drop (+ idx (count sep)) coll)))
|
|
(list coll))))))
|
|
|
|
;; if you need a refresher what multipart messages look like:
|
|
;; https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
|
|
|
|
(defn parse-multipart-alternative [body]
|
|
(let [parsed (partition-with-seq (map int [\return \newline \return \newline]) body)]
|
|
{:header (apply str (map char (first parsed)))
|
|
:body (byte-array (apply concat (rest parsed)))}))
|
|
|
|
(defn parse-multipart [request]
|
|
(let [content-type (get-in request [:headers "Content-Type"])
|
|
boundary (str "--" (second (re-find #"boundary=\"(.*?)\"" content-type)) "\r\n")]
|
|
;; let's throw in an assert because we have no idea how other servers
|
|
;; implement streaming or wether they implement it at all
|
|
(assert (some? boundary) "Could not parse multipart/x-mixed-replace boundary")
|
|
(with-open [input (:body request)]
|
|
;; find indices of the bytes between the first and second boundary; the byte
|
|
;; sequence always starts with the boundary, which is why can skip the first
|
|
;; byte and have this find the end index
|
|
(let [byte-seq (input->byte-seq input)
|
|
boundary-seq (map int boundary)]
|
|
;; the multipart message is prepended by the boundary, so we discard the
|
|
;; first (empty) split
|
|
(parse-multipart-alternative (second (partition-with-seq boundary-seq byte-seq)))))))
|
|
|
|
(def first-multipart-chunk (parse-multipart (http/get some-camera-image {:as :stream})))
|
|
|
|
;; we need javax to convert the byte array that is contained in the body of the
|
|
;; first multipart alternative to a `BufferedImage` that we can use
|
|
;; with Clojure2d.
|
|
|
|
(require '[clojure2d.core :as c2d])
|
|
|
|
(defn byte-array->image [bs]
|
|
(with-open [in (java.io.ByteArrayInputStream. bs)]
|
|
(javax.imageio.ImageIO/read in)))
|
|
|
|
(def img (byte-array->image (:body first-multipart-chunk)))
|
|
(def canvas (c2d/canvas (c2d/width img) (c2d/height img)))
|
|
|
|
(c2d/with-canvas [c canvas]
|
|
(c2d/image c img))
|
|
|
|
(c2d/show-window canvas "Hello World")
|