├── .gitignore ├── CapsLockState.h ├── CapsLockStateOSX.cpp ├── CapsLockStateX11.cpp ├── CaptureConfig.cpp ├── CaptureConfig.h ├── Cursor.h ├── CursorCommon.cpp ├── CursorOSX.cpp ├── CursorX11.cpp ├── LICENSE.txt ├── MurmurHash3.cpp ├── MurmurHash3.h ├── ObjectiveCBridge.h ├── ObjectiveCBridge.m ├── README.md ├── example ├── cursor_1928810032.png ├── cursor_2544569486.png ├── example.html ├── example.js ├── example_packed.js ├── example_packed.png ├── sample_1356229702967.png └── sample_1356229706163.png ├── main.cpp ├── pack_animation.py ├── player.js └── screencast.pro /.gitignore: -------------------------------------------------------------------------------- 1 | *.pro.user 2 | *.o 3 | *.pyc 4 | screencast 5 | Makefile 6 | -------------------------------------------------------------------------------- /CapsLockState.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREENCAST_CAPSLOCKSTATE_H 2 | #define SCREENCAST_CAPSLOCKSTATE_H 3 | 4 | namespace screencast { 5 | 6 | bool capsLockEnabled(); 7 | 8 | } // namespace screencast 9 | 10 | #endif // SCREENCAST_CAPSLOCKSTATE_H 11 | -------------------------------------------------------------------------------- /CapsLockStateOSX.cpp: -------------------------------------------------------------------------------- 1 | #include "CapsLockState.h" 2 | 3 | #include "ObjectiveCBridge.h" 4 | 5 | namespace screencast { 6 | 7 | bool capsLockEnabled() 8 | { 9 | return bridgeCapsLockEnabled(); 10 | } 11 | 12 | } // namespace screencast 13 | -------------------------------------------------------------------------------- /CapsLockStateX11.cpp: -------------------------------------------------------------------------------- 1 | #include "CapsLockState.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace screencast { 8 | 9 | bool capsLockEnabled() 10 | { 11 | unsigned int n = 0; 12 | XkbGetIndicatorState(QX11Info::display(), XkbUseCoreKbd, &n); 13 | return (n & 0x01) == 1; 14 | } 15 | 16 | } // namespace screencast 17 | -------------------------------------------------------------------------------- /CaptureConfig.cpp: -------------------------------------------------------------------------------- 1 | #include "CaptureConfig.h" 2 | -------------------------------------------------------------------------------- /CaptureConfig.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREENCAST_CAPTURECONFIG_H 2 | #define SCREENCAST_CAPTURECONFIG_H 3 | 4 | namespace screencast { 5 | 6 | class CaptureConfig 7 | { 8 | public: 9 | CaptureConfig() : 10 | captureX(0), captureY(0), 11 | captureWidth(0), captureHeight(0) 12 | { 13 | } 14 | 15 | int captureX; 16 | int captureY; 17 | int captureWidth; 18 | int captureHeight; 19 | }; 20 | 21 | } // namespace screencast 22 | 23 | #endif // SCREENCAST_CAPTURECONFIG_H 24 | -------------------------------------------------------------------------------- /Cursor.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREENCAST_CURSOR_H 2 | #define SCREENCAST_CURSOR_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace screencast { 10 | 11 | class CaptureConfig; 12 | 13 | class Cursor 14 | { 15 | public: 16 | Cursor(); 17 | Cursor(const CaptureConfig &config); 18 | QPoint position() { return m_position; } 19 | uint32_t imageID() { return m_imageID; } 20 | 21 | private: 22 | static uint32_t blankCursor(); 23 | 24 | private: 25 | QPoint m_position; 26 | uint32_t m_imageID; 27 | static std::set m_cachedImages; 28 | }; 29 | 30 | } // namespace screencast 31 | 32 | #endif // SCREENCAST_CURSOR_H 33 | -------------------------------------------------------------------------------- /CursorCommon.cpp: -------------------------------------------------------------------------------- 1 | #include "Cursor.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace screencast { 12 | 13 | uint32_t Cursor::blankCursor() 14 | { 15 | const uint32_t blankId = 0; 16 | 17 | if (m_cachedImages.find(blankId) == m_cachedImages.end()) { 18 | uint32_t pixel = 0; 19 | QImage mouseCursor( 20 | reinterpret_cast(&pixel), 21 | 1, 1, 22 | QImage::Format_ARGB32_Premultiplied); 23 | mouseCursor.save(QString("cursor_%0.png").arg(blankId)); 24 | m_cachedImages.insert(blankId); 25 | } 26 | return blankId; 27 | } 28 | 29 | } // namespace screencast 30 | -------------------------------------------------------------------------------- /CursorOSX.cpp: -------------------------------------------------------------------------------- 1 | #include "Cursor.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | #include "CaptureConfig.h" 11 | #include "MurmurHash3.h" 12 | #include "ObjectiveCBridge.h" 13 | 14 | namespace screencast { 15 | 16 | std::set Cursor::m_cachedImages; 17 | 18 | Cursor::Cursor() : m_imageID(0) 19 | { 20 | } 21 | 22 | Cursor::Cursor(const CaptureConfig &config) 23 | { 24 | id cursor = cursor_currentSystemCursor(); 25 | BridgePoint hotSpot = cursor_hotSpot(cursor); 26 | BridgePoint mouseLocation = event_mouseLocation(); 27 | id cursorImage = cursor_image(cursor); 28 | id tiffData = cursorImage != nil ? 29 | image_TIFFRepresentation(cursorImage) : nil; 30 | 31 | if (tiffData == nil) { 32 | m_imageID = Cursor::blankCursor(); 33 | m_position.rx() = 0; 34 | m_position.ry() = 0; 35 | return; 36 | } 37 | 38 | MurmurHash3_x86_32( 39 | data_bytes(tiffData), 40 | data_length(tiffData), 41 | 0, 42 | &m_imageID); 43 | 44 | if (m_cachedImages.find(m_imageID) == m_cachedImages.end()) { 45 | m_cachedImages.insert(m_imageID); 46 | bridgeWriteCursorFile(tiffData, m_imageID); 47 | } 48 | 49 | // The NSCursor Y coordinate's origin is at the bottom of the screen. 50 | // Invert it. 51 | mouseLocation.y = 52 | QApplication::desktop()->screenGeometry().height() - mouseLocation.y; 53 | 54 | m_position.rx() = mouseLocation.x - hotSpot.x - config.captureX; 55 | m_position.ry() = mouseLocation.y - hotSpot.y - config.captureY; 56 | } 57 | 58 | } // namespace screencast 59 | -------------------------------------------------------------------------------- /CursorX11.cpp: -------------------------------------------------------------------------------- 1 | #include "Cursor.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | #include "CaptureConfig.h" 17 | #include "MurmurHash3.h" 18 | 19 | namespace screencast { 20 | 21 | std::set Cursor::m_cachedImages; 22 | 23 | Cursor::Cursor() : m_imageID(0) 24 | { 25 | } 26 | 27 | Cursor::Cursor(const CaptureConfig &config) 28 | { 29 | XFixesCursorImage *cursor = XFixesGetCursorImage(QX11Info::display()); 30 | std::vector pixels(cursor->width * cursor->height); 31 | // The X11 data is encoded as one-pixel-per-long, but each pixel is only 32 | // 32 bits of data, so every other 4 bytes is padding. The Qt API is more 33 | // reasonable and expects one-pixel-per-32-bits, so copy the pixels. I 34 | // wonder whether endianness is an issue here. 35 | for (size_t i = 0; i < pixels.size(); ++i) 36 | pixels[i] = cursor->pixels[i]; 37 | MurmurHash3_x86_32( 38 | pixels.data(), 39 | pixels.size() * sizeof(uint32_t), 40 | 0, 41 | &m_imageID); 42 | if (m_cachedImages.find(m_imageID) == m_cachedImages.end()) { 43 | QImage mouseCursor((unsigned char*)pixels.data(), 44 | cursor->width, cursor->height, 45 | QImage::Format_ARGB32_Premultiplied); 46 | mouseCursor.save(QString("cursor_%0.png").arg(m_imageID)); 47 | m_cachedImages.insert(m_imageID); 48 | } 49 | m_position.rx() = cursor->x - cursor->xhot - config.captureX; 50 | m_position.ry() = cursor->y - cursor->yhot - config.captureY; 51 | XFree(cursor); 52 | } 53 | 54 | } // namespace screencast 55 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Ryan Prichard 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | * Neither the name of the nor the 11 | names of its contributors may be used to endorse or promote products 12 | derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 18 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MurmurHash3.cpp: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // MurmurHash3 was written by Austin Appleby, and is placed in the public 3 | // domain. The author hereby disclaims copyright to this source code. 4 | 5 | // Note - The x86 and x64 versions do _not_ produce the same results, as the 6 | // algorithms are optimized for their respective platforms. You can still 7 | // compile and run any of them on any platform, but your performance with the 8 | // non-native version will be less than optimal. 9 | 10 | #include "MurmurHash3.h" 11 | 12 | //----------------------------------------------------------------------------- 13 | // Platform-specific functions and macros 14 | 15 | // Microsoft Visual Studio 16 | 17 | #if defined(_MSC_VER) 18 | 19 | #define FORCE_INLINE __forceinline 20 | 21 | #include 22 | 23 | #define ROTL32(x,y) _rotl(x,y) 24 | #define ROTL64(x,y) _rotl64(x,y) 25 | 26 | #define BIG_CONSTANT(x) (x) 27 | 28 | // Other compilers 29 | 30 | #else // defined(_MSC_VER) 31 | 32 | #define FORCE_INLINE __attribute__((always_inline)) 33 | 34 | inline uint32_t rotl32 ( uint32_t x, int8_t r ) 35 | { 36 | return (x << r) | (x >> (32 - r)); 37 | } 38 | 39 | inline uint64_t rotl64 ( uint64_t x, int8_t r ) 40 | { 41 | return (x << r) | (x >> (64 - r)); 42 | } 43 | 44 | #define ROTL32(x,y) rotl32(x,y) 45 | #define ROTL64(x,y) rotl64(x,y) 46 | 47 | #define BIG_CONSTANT(x) (x##LLU) 48 | 49 | #endif // !defined(_MSC_VER) 50 | 51 | //----------------------------------------------------------------------------- 52 | // Block read - if your platform needs to do endian-swapping or can only 53 | // handle aligned reads, do the conversion here 54 | 55 | FORCE_INLINE uint32_t getblock ( const uint32_t * p, int i ) 56 | { 57 | return p[i]; 58 | } 59 | 60 | FORCE_INLINE uint64_t getblock ( const uint64_t * p, int i ) 61 | { 62 | return p[i]; 63 | } 64 | 65 | //----------------------------------------------------------------------------- 66 | // Finalization mix - force all bits of a hash block to avalanche 67 | 68 | FORCE_INLINE uint32_t fmix ( uint32_t h ) 69 | { 70 | h ^= h >> 16; 71 | h *= 0x85ebca6b; 72 | h ^= h >> 13; 73 | h *= 0xc2b2ae35; 74 | h ^= h >> 16; 75 | 76 | return h; 77 | } 78 | 79 | //---------- 80 | 81 | FORCE_INLINE uint64_t fmix ( uint64_t k ) 82 | { 83 | k ^= k >> 33; 84 | k *= BIG_CONSTANT(0xff51afd7ed558ccd); 85 | k ^= k >> 33; 86 | k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); 87 | k ^= k >> 33; 88 | 89 | return k; 90 | } 91 | 92 | //----------------------------------------------------------------------------- 93 | 94 | void MurmurHash3_x86_32 ( const void * key, int len, 95 | uint32_t seed, void * out ) 96 | { 97 | const uint8_t * data = (const uint8_t*)key; 98 | const int nblocks = len / 4; 99 | 100 | uint32_t h1 = seed; 101 | 102 | const uint32_t c1 = 0xcc9e2d51; 103 | const uint32_t c2 = 0x1b873593; 104 | 105 | //---------- 106 | // body 107 | 108 | const uint32_t * blocks = (const uint32_t *)(data + nblocks*4); 109 | 110 | for(int i = -nblocks; i; i++) 111 | { 112 | uint32_t k1 = getblock(blocks,i); 113 | 114 | k1 *= c1; 115 | k1 = ROTL32(k1,15); 116 | k1 *= c2; 117 | 118 | h1 ^= k1; 119 | h1 = ROTL32(h1,13); 120 | h1 = h1*5+0xe6546b64; 121 | } 122 | 123 | //---------- 124 | // tail 125 | 126 | const uint8_t * tail = (const uint8_t*)(data + nblocks*4); 127 | 128 | uint32_t k1 = 0; 129 | 130 | switch(len & 3) 131 | { 132 | case 3: k1 ^= tail[2] << 16; 133 | case 2: k1 ^= tail[1] << 8; 134 | case 1: k1 ^= tail[0]; 135 | k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; 136 | }; 137 | 138 | //---------- 139 | // finalization 140 | 141 | h1 ^= len; 142 | 143 | h1 = fmix(h1); 144 | 145 | *(uint32_t*)out = h1; 146 | } 147 | 148 | //----------------------------------------------------------------------------- 149 | 150 | void MurmurHash3_x86_128 ( const void * key, const int len, 151 | uint32_t seed, void * out ) 152 | { 153 | const uint8_t * data = (const uint8_t*)key; 154 | const int nblocks = len / 16; 155 | 156 | uint32_t h1 = seed; 157 | uint32_t h2 = seed; 158 | uint32_t h3 = seed; 159 | uint32_t h4 = seed; 160 | 161 | const uint32_t c1 = 0x239b961b; 162 | const uint32_t c2 = 0xab0e9789; 163 | const uint32_t c3 = 0x38b34ae5; 164 | const uint32_t c4 = 0xa1e38b93; 165 | 166 | //---------- 167 | // body 168 | 169 | const uint32_t * blocks = (const uint32_t *)(data + nblocks*16); 170 | 171 | for(int i = -nblocks; i; i++) 172 | { 173 | uint32_t k1 = getblock(blocks,i*4+0); 174 | uint32_t k2 = getblock(blocks,i*4+1); 175 | uint32_t k3 = getblock(blocks,i*4+2); 176 | uint32_t k4 = getblock(blocks,i*4+3); 177 | 178 | k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; 179 | 180 | h1 = ROTL32(h1,19); h1 += h2; h1 = h1*5+0x561ccd1b; 181 | 182 | k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; 183 | 184 | h2 = ROTL32(h2,17); h2 += h3; h2 = h2*5+0x0bcaa747; 185 | 186 | k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; 187 | 188 | h3 = ROTL32(h3,15); h3 += h4; h3 = h3*5+0x96cd1c35; 189 | 190 | k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; 191 | 192 | h4 = ROTL32(h4,13); h4 += h1; h4 = h4*5+0x32ac3b17; 193 | } 194 | 195 | //---------- 196 | // tail 197 | 198 | const uint8_t * tail = (const uint8_t*)(data + nblocks*16); 199 | 200 | uint32_t k1 = 0; 201 | uint32_t k2 = 0; 202 | uint32_t k3 = 0; 203 | uint32_t k4 = 0; 204 | 205 | switch(len & 15) 206 | { 207 | case 15: k4 ^= tail[14] << 16; 208 | case 14: k4 ^= tail[13] << 8; 209 | case 13: k4 ^= tail[12] << 0; 210 | k4 *= c4; k4 = ROTL32(k4,18); k4 *= c1; h4 ^= k4; 211 | 212 | case 12: k3 ^= tail[11] << 24; 213 | case 11: k3 ^= tail[10] << 16; 214 | case 10: k3 ^= tail[ 9] << 8; 215 | case 9: k3 ^= tail[ 8] << 0; 216 | k3 *= c3; k3 = ROTL32(k3,17); k3 *= c4; h3 ^= k3; 217 | 218 | case 8: k2 ^= tail[ 7] << 24; 219 | case 7: k2 ^= tail[ 6] << 16; 220 | case 6: k2 ^= tail[ 5] << 8; 221 | case 5: k2 ^= tail[ 4] << 0; 222 | k2 *= c2; k2 = ROTL32(k2,16); k2 *= c3; h2 ^= k2; 223 | 224 | case 4: k1 ^= tail[ 3] << 24; 225 | case 3: k1 ^= tail[ 2] << 16; 226 | case 2: k1 ^= tail[ 1] << 8; 227 | case 1: k1 ^= tail[ 0] << 0; 228 | k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; 229 | }; 230 | 231 | //---------- 232 | // finalization 233 | 234 | h1 ^= len; h2 ^= len; h3 ^= len; h4 ^= len; 235 | 236 | h1 += h2; h1 += h3; h1 += h4; 237 | h2 += h1; h3 += h1; h4 += h1; 238 | 239 | h1 = fmix(h1); 240 | h2 = fmix(h2); 241 | h3 = fmix(h3); 242 | h4 = fmix(h4); 243 | 244 | h1 += h2; h1 += h3; h1 += h4; 245 | h2 += h1; h3 += h1; h4 += h1; 246 | 247 | ((uint32_t*)out)[0] = h1; 248 | ((uint32_t*)out)[1] = h2; 249 | ((uint32_t*)out)[2] = h3; 250 | ((uint32_t*)out)[3] = h4; 251 | } 252 | 253 | //----------------------------------------------------------------------------- 254 | 255 | void MurmurHash3_x64_128 ( const void * key, const int len, 256 | const uint32_t seed, void * out ) 257 | { 258 | const uint8_t * data = (const uint8_t*)key; 259 | const int nblocks = len / 16; 260 | 261 | uint64_t h1 = seed; 262 | uint64_t h2 = seed; 263 | 264 | const uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); 265 | const uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f); 266 | 267 | //---------- 268 | // body 269 | 270 | const uint64_t * blocks = (const uint64_t *)(data); 271 | 272 | for(int i = 0; i < nblocks; i++) 273 | { 274 | uint64_t k1 = getblock(blocks,i*2+0); 275 | uint64_t k2 = getblock(blocks,i*2+1); 276 | 277 | k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1; 278 | 279 | h1 = ROTL64(h1,27); h1 += h2; h1 = h1*5+0x52dce729; 280 | 281 | k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2; 282 | 283 | h2 = ROTL64(h2,31); h2 += h1; h2 = h2*5+0x38495ab5; 284 | } 285 | 286 | //---------- 287 | // tail 288 | 289 | const uint8_t * tail = (const uint8_t*)(data + nblocks*16); 290 | 291 | uint64_t k1 = 0; 292 | uint64_t k2 = 0; 293 | 294 | switch(len & 15) 295 | { 296 | case 15: k2 ^= uint64_t(tail[14]) << 48; 297 | case 14: k2 ^= uint64_t(tail[13]) << 40; 298 | case 13: k2 ^= uint64_t(tail[12]) << 32; 299 | case 12: k2 ^= uint64_t(tail[11]) << 24; 300 | case 11: k2 ^= uint64_t(tail[10]) << 16; 301 | case 10: k2 ^= uint64_t(tail[ 9]) << 8; 302 | case 9: k2 ^= uint64_t(tail[ 8]) << 0; 303 | k2 *= c2; k2 = ROTL64(k2,33); k2 *= c1; h2 ^= k2; 304 | 305 | case 8: k1 ^= uint64_t(tail[ 7]) << 56; 306 | case 7: k1 ^= uint64_t(tail[ 6]) << 48; 307 | case 6: k1 ^= uint64_t(tail[ 5]) << 40; 308 | case 5: k1 ^= uint64_t(tail[ 4]) << 32; 309 | case 4: k1 ^= uint64_t(tail[ 3]) << 24; 310 | case 3: k1 ^= uint64_t(tail[ 2]) << 16; 311 | case 2: k1 ^= uint64_t(tail[ 1]) << 8; 312 | case 1: k1 ^= uint64_t(tail[ 0]) << 0; 313 | k1 *= c1; k1 = ROTL64(k1,31); k1 *= c2; h1 ^= k1; 314 | }; 315 | 316 | //---------- 317 | // finalization 318 | 319 | h1 ^= len; h2 ^= len; 320 | 321 | h1 += h2; 322 | h2 += h1; 323 | 324 | h1 = fmix(h1); 325 | h2 = fmix(h2); 326 | 327 | h1 += h2; 328 | h2 += h1; 329 | 330 | ((uint64_t*)out)[0] = h1; 331 | ((uint64_t*)out)[1] = h2; 332 | } 333 | 334 | //----------------------------------------------------------------------------- 335 | 336 | -------------------------------------------------------------------------------- /MurmurHash3.h: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // MurmurHash3 was written by Austin Appleby, and is placed in the public 3 | // domain. The author hereby disclaims copyright to this source code. 4 | 5 | #ifndef _MURMURHASH3_H_ 6 | #define _MURMURHASH3_H_ 7 | 8 | //----------------------------------------------------------------------------- 9 | // Platform-specific functions and macros 10 | 11 | // Microsoft Visual Studio 12 | 13 | #if defined(_MSC_VER) 14 | 15 | typedef unsigned char uint8_t; 16 | typedef unsigned long uint32_t; 17 | typedef unsigned __int64 uint64_t; 18 | 19 | // Other compilers 20 | 21 | #else // defined(_MSC_VER) 22 | 23 | #include 24 | 25 | #endif // !defined(_MSC_VER) 26 | 27 | //----------------------------------------------------------------------------- 28 | 29 | void MurmurHash3_x86_32 ( const void * key, int len, uint32_t seed, void * out ); 30 | 31 | void MurmurHash3_x86_128 ( const void * key, int len, uint32_t seed, void * out ); 32 | 33 | void MurmurHash3_x64_128 ( const void * key, int len, uint32_t seed, void * out ); 34 | 35 | //----------------------------------------------------------------------------- 36 | 37 | #endif // _MURMURHASH3_H_ 38 | -------------------------------------------------------------------------------- /ObjectiveCBridge.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREENCAST_OBJECTIVECBRIDGE_H 2 | #define SCREENCAST_OBJECTIVECBRIDGE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | typedef struct { 14 | double x; 15 | double y; 16 | } BridgePoint; 17 | 18 | id cursor_currentSystemCursor(); 19 | BridgePoint cursor_hotSpot(id cursor); 20 | BridgePoint event_mouseLocation(); 21 | id cursor_image(id cursor); 22 | id image_TIFFRepresentation(id image); 23 | const void *data_bytes(id data); 24 | size_t data_length(id data); 25 | void bridgeWriteCursorFile(id tiffData, uint32_t imageID); 26 | bool bridgeCapsLockEnabled(); 27 | 28 | #ifdef __cplusplus 29 | } 30 | #endif 31 | 32 | #endif // SCREENCAST_OBJECTIVECBRIDGE_H 33 | -------------------------------------------------------------------------------- /ObjectiveCBridge.m: -------------------------------------------------------------------------------- 1 | // qmake does not compile Objective-C++ sources with C++ flags. This is a 2 | // critical failing, because the C++ flags can alter the library ABI. In 3 | // particular, -stdlib=libc++ enables an incompatible standard library. 4 | // 5 | // I do not see an obvious non-hacky fix for this issue, and without one, 6 | // Objective-C++ is unusable with qmake, at least on OS X with the version of 7 | // Qt I'm using (Qt 4.8.6 installed via Homebrew). 8 | // 9 | // As a workaround, put Objective-C code here and call it from C++. 10 | 11 | #import "ObjectiveCBridge.h" 12 | 13 | #import 14 | #import 15 | 16 | id cursor_currentSystemCursor() 17 | { 18 | return [NSCursor currentSystemCursor]; 19 | } 20 | 21 | BridgePoint cursor_hotSpot(id cursor) 22 | { 23 | NSPoint ret = [(NSCursor*)cursor hotSpot]; 24 | BridgePoint ret2 = { ret.x, ret.y }; 25 | return ret2; 26 | } 27 | 28 | BridgePoint event_mouseLocation() 29 | { 30 | NSPoint ret = [NSEvent mouseLocation]; 31 | BridgePoint ret2 = { ret.x, ret.y }; 32 | return ret2; 33 | } 34 | 35 | id cursor_image(id cursor) 36 | { 37 | return [(NSCursor*)cursor image]; 38 | } 39 | 40 | id image_TIFFRepresentation(id image) 41 | { 42 | return [(NSImage*)image TIFFRepresentation]; 43 | } 44 | 45 | const void *data_bytes(id data) 46 | { 47 | return [(NSData*)data bytes]; 48 | } 49 | 50 | size_t data_length(id data) 51 | { 52 | return [(NSData*)data length]; 53 | } 54 | 55 | void bridgeWriteCursorFile(id tiffData, uint32_t imageID) 56 | { 57 | NSBitmapImageRep *imageRep = 58 | [[[NSBitmapImageRep alloc] 59 | initWithData:(NSData*)tiffData] autorelease]; 60 | assert(imageRep != nil); 61 | NSData *pngData = 62 | [imageRep representationUsingType:NSPNGFileType properties:nil]; 63 | NSString *path = 64 | [NSString stringWithFormat:@"cursor_%u.png", imageID]; 65 | [pngData writeToFile:path atomically:NO]; 66 | } 67 | 68 | bool bridgeCapsLockEnabled() 69 | { 70 | return ([NSEvent modifierFlags] & NSAlphaShiftKeyMask) != 0; 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | x11-canvas-screencast 2 | ===================== 3 | 4 | x11-canvas-screencast is a UNIX-to-HTML5-Canvas screencasting system that uses 5 | the same animation technique as the [anim_encoder][1] project. 6 | 7 | [1]: https://www.github.com/sublimehq/anim_encoder 8 | 9 | This project's name is now a misnomer. On OS X, it does not use X11 at all. 10 | 11 | The project includes a Qt executable, `screencast`, which polls the screen 12 | and mouse cursor and outputs an animation. Specifically, it writes an 13 | animation script (a CSV-separated list of steps) and a number of PNG files (one 14 | per screen capture and one per unique mouse cursor). The `pack_animation.py` 15 | script (based on `anim_encoder.py`) optimizes an animation script by creating 16 | a packed PNG file and then replacing each `screen` command with a `blit` 17 | command that copies part of the packed PNG file into the canvas. 18 | 19 | Packing the animation can be slow, and this approach makes it easy to view and 20 | tweak the animation prior to packing it. 21 | 22 | Prerequisites 23 | ------------- 24 | 25 | The `screencast` program depends on Qt. On targets other than OS X, it also 26 | needs the XFixes extension to query the mouse cursor. The `pack_animation.py` 27 | script has the same dependencies as `anim_encoder` -- NumPy, SciPy, OpenCV, and 28 | pngcrush. On Ubuntu, install these packages: 29 | 30 | libqt4-dev libxfixes-dev python-numpy python-scipy python-opencv pngcrush 31 | 32 | Usage 33 | ----- 34 | 35 | 1. Build `screencast`: 36 | 37 |
38 |     $ qmake
39 |     $ make
40 |     
41 | 42 | 2. Capture an animation: 43 | 44 |
45 |     $ ./screencast --rect X Y W H --output example.js
46 |     
47 | 48 | Hit Enter to stop the capture. Turning on CAPS LOCK will temporarily pause 49 | the capture. Turning off CAPS LOCK returns the mouse cursor to its position 50 | before pausing the capture, then unpauses the capture. 51 | 52 | 3. Pack the animation: 53 | 54 |
55 |     $ ./pack_animation.py example.js
56 |     
57 | 58 | `pack_animation.py example.js` will output `example_packed.js` and 59 | `example_packed.png`. 60 | 61 | Embedding an animation in a web page 62 | ------------------------------------ 63 | 64 | See `example/example.html` for an example of embedding the player. 65 | 66 | Include the animation's JavaScript file and `player.js` in the page. The 67 | `player.js` script defines a Player class. Construct it: 68 | 69 | var player = Player(, ""); 70 | 71 | The `` path will be prefixed to each path in the 72 | animation script. The `Player` object has an `element` field. Add or remove 73 | it to a page. A player is initially paused; unpause it with the `start` 74 | method. Pause it with the `pause` method. 75 | 76 | The `Player` object has two events: 77 | 78 | - `onload`. This is called after all of the images are loaded, and after 79 | the player's canvas has been painted with the first frame. 80 | 81 | - `onloop`. This is called at the end of the animation, as it is looping back 82 | to the beginning. Pause it here to prevent looping. 83 | 84 | Caveats 85 | ------- 86 | 87 | I have not tested the `screencast` program with multiple screens or with 88 | high-DPI/Retina screens. Either situation could break badly. 89 | 90 | On OS X, I have tested the `screencast` program, but not `pack_animation.py`. 91 | It will probably work fine if the Python dependencies are satisfied. 92 | 93 | License 94 | ------- 95 | 96 | BSD license. 97 | -------------------------------------------------------------------------------- /example/cursor_1928810032.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rprichard/x11-canvas-screencast/d8b6ec7f81ae0c84ce62c2cb206de6d987206d82/example/cursor_1928810032.png -------------------------------------------------------------------------------- /example/cursor_2544569486.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rprichard/x11-canvas-screencast/d8b6ec7f81ae0c84ce62c2cb206de6d987206d82/example/cursor_2544569486.png -------------------------------------------------------------------------------- /example/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | start
11 | pause
12 | 13 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | example = { 2 | "width" : 950, 3 | "height" : 500, 4 | "steps" : [ 5 | 6 | // Change the first sample to have 0-delay. (This could be fixed in the 7 | // capture program.) 8 | [0,"screen","sample_1356229702967.png"], 9 | 10 | [0,"cpos",41,151], 11 | [0,"cimg","cursor_2544569486.png"], 12 | [30,"cpos",44,153], 13 | [30,"cpos",49,154], 14 | [30,"cpos",55,156], 15 | [30,"cpos",61,157], 16 | [30,"cpos",71,158], 17 | [30,"cpos",79,158], 18 | [30,"cpos",86,159], 19 | [30,"cpos",96,159], 20 | [30,"cpos",102,159], 21 | [30,"cpos",108,160], 22 | [30,"cpos",116,159], 23 | [30,"cpos",124,160], 24 | [30,"cpos",131,158], 25 | [30,"cpos",135,158], 26 | [30,"cpos",139,156], 27 | [30,"cpos",143,154], 28 | [30,"cpos",147,151], 29 | [30,"cpos",152,150], 30 | [30,"cpos",155,148], 31 | [30,"cpos",156,146], 32 | [30,"cpos",157,142], 33 | [30,"cpos",160,139], 34 | [30,"cpos",161,134], 35 | [30,"cpos",162,130], 36 | [30,"cpos",165,125], 37 | [30,"cpos",167,121], 38 | [30,"cpos",168,118], 39 | [30,"cpos",168,115], 40 | [30,"cpos",168,114], 41 | [30,"cpos",168,112], 42 | [30,"cpos",172,117], 43 | [0,"cimg","cursor_1928810032.png"], 44 | [30,"cpos",172,115], 45 | [100,"screen","sample_1356229706163.png"], 46 | [0,"cpos",172,112], 47 | [30,"cpos",172,109], 48 | [30,"cpos",172,108], 49 | 50 | // Add a delay at the end by copying the cpos. (There should probably be a nop 51 | // action.) 52 | [300,"cpos",172,108], 53 | 54 | ]}; 55 | -------------------------------------------------------------------------------- /example/example_packed.js: -------------------------------------------------------------------------------- 1 | example_packed = { 2 | "width" : 950, 3 | "height" : 500, 4 | "steps" : [ 5 | [0, "blitimg", "example_packed.png"], 6 | [0, "blit", [[0, 0, 950, 500, 0, 0]]], 7 | [0, "cpos", 41, 151], 8 | [0, "cimg", "cursor_2544569486.png"], 9 | [30, "cpos", 44, 153], 10 | [30, "cpos", 49, 154], 11 | [30, "cpos", 55, 156], 12 | [30, "cpos", 61, 157], 13 | [30, "cpos", 71, 158], 14 | [30, "cpos", 79, 158], 15 | [30, "cpos", 86, 159], 16 | [30, "cpos", 96, 159], 17 | [30, "cpos", 102, 159], 18 | [30, "cpos", 108, 160], 19 | [30, "cpos", 116, 159], 20 | [30, "cpos", 124, 160], 21 | [30, "cpos", 131, 158], 22 | [30, "cpos", 135, 158], 23 | [30, "cpos", 139, 156], 24 | [30, "cpos", 143, 154], 25 | [30, "cpos", 147, 151], 26 | [30, "cpos", 152, 150], 27 | [30, "cpos", 155, 148], 28 | [30, "cpos", 156, 146], 29 | [30, "cpos", 157, 142], 30 | [30, "cpos", 160, 139], 31 | [30, "cpos", 161, 134], 32 | [30, "cpos", 162, 130], 33 | [30, "cpos", 165, 125], 34 | [30, "cpos", 167, 121], 35 | [30, "cpos", 168, 118], 36 | [30, "cpos", 168, 115], 37 | [30, "cpos", 168, 114], 38 | [30, "cpos", 168, 112], 39 | [30, "cpos", 172, 117], 40 | [0, "cimg", "cursor_1928810032.png"], 41 | [30, "cpos", 172, 115], 42 | [100, "blit", [[0, 500, 18, 18, 171, 100]]], 43 | [0, "cpos", 172, 112], 44 | [30, "cpos", 172, 109], 45 | [30, "cpos", 172, 108], 46 | [300, "cpos", 172, 108], 47 | ]}; 48 | -------------------------------------------------------------------------------- /example/example_packed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rprichard/x11-canvas-screencast/d8b6ec7f81ae0c84ce62c2cb206de6d987206d82/example/example_packed.png -------------------------------------------------------------------------------- /example/sample_1356229702967.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rprichard/x11-canvas-screencast/d8b6ec7f81ae0c84ce62c2cb206de6d987206d82/example/sample_1356229702967.png -------------------------------------------------------------------------------- /example/sample_1356229706163.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rprichard/x11-canvas-screencast/d8b6ec7f81ae0c84ce62c2cb206de6d987206d82/example/sample_1356229706163.png -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "CapsLockState.h" 20 | #include "CaptureConfig.h" 21 | #include "Cursor.h" 22 | #include "MurmurHash3.h" 23 | 24 | volatile bool g_finish = false; 25 | 26 | static void sleepMS(int ms) 27 | { 28 | timespec ts = { ms / 1000, (ms % 1000) * 1000 * 1000 }; 29 | nanosleep(&ts, NULL); 30 | } 31 | 32 | static void usage(const char *argv0) 33 | { 34 | printf("Usage: %s --rect X Y W H --output OUTFILE\n", argv0); 35 | } 36 | 37 | static bool stringEndsWith(const std::string &str, const std::string &suffix) 38 | { 39 | if (suffix.size() > str.size()) 40 | return false; 41 | return !str.compare(str.size() - suffix.size(), suffix.size(), suffix); 42 | } 43 | 44 | int main(int argc, char *argv[]) 45 | { 46 | QApplication app(argc, argv); 47 | screencast::CaptureConfig config; 48 | std::string output; 49 | std::string baseName; 50 | 51 | for (int i = 1; i < argc; ) { 52 | const char *arg = argv[i]; 53 | if (!strcmp(arg, "--rect") && (i + 4 < argc)) { 54 | config.captureX = atoi(argv[i + 1]); 55 | config.captureY = atoi(argv[i + 2]); 56 | config.captureWidth = atoi(argv[i + 3]); 57 | config.captureHeight = atoi(argv[i + 4]); 58 | i += 5; 59 | } else if (!strcmp(arg, "--help")) { 60 | usage(argv[0]); 61 | exit(0); 62 | } else if (!strcmp(arg, "--output") && (i + 1 < argc)) { 63 | output = argv[i + 1]; 64 | if (!stringEndsWith(output, ".js")) { 65 | fprintf(stderr, "Output file must end with .js extension.\n"); 66 | exit(1); 67 | } 68 | baseName = output.substr(0, output.size() - 3); 69 | i += 2; 70 | } else { 71 | fprintf(stderr, "error: Unrecognized argument: %s\n", argv[i]); 72 | usage(argv[0]); 73 | exit(1); 74 | } 75 | } 76 | if (config.captureWidth == 0 || config.captureHeight == 0) { 77 | fprintf(stderr, 78 | "error: A capture rectangle must be specified with --rect.\n"); 79 | usage(argv[0]); 80 | exit(1); 81 | } 82 | if (output.empty()) { 83 | fprintf(stderr, 84 | "error: An output file must be specified with --output.\n"); 85 | usage(argv[0]); 86 | exit(1); 87 | } 88 | 89 | FILE *fp = fopen(output.c_str(), "wb"); 90 | if (!fp) { 91 | fprintf(stderr, "error: Could not open %s\n", output.c_str()); 92 | exit(1); 93 | } 94 | setvbuf(fp, NULL, _IOLBF, 0); 95 | fprintf(fp, "%s = {\n", baseName.c_str()); 96 | fprintf(fp, "\"width\": %d,\n", config.captureWidth); 97 | fprintf(fp, "\"height\": %d,\n", config.captureHeight); 98 | fprintf(fp, "\"steps\": [\n"); 99 | 100 | printf("Starting in 2 seconds.\n"); 101 | sleep(2); 102 | printf("Now recording -- press ENTER to stop.\n"); 103 | 104 | QImage previousImage; 105 | screencast::Cursor previousCursor; 106 | bool previousFrozen = false; 107 | QPoint frozenMousePosition; 108 | 109 | { 110 | int flags = fcntl(STDIN_FILENO, F_GETFL, 0); 111 | flags |= O_NONBLOCK; 112 | fcntl(STDIN_FILENO, F_SETFL, flags); 113 | } 114 | 115 | const int FPS = 60; 116 | const int maxDelayMS = 250; 117 | bool firstFrame = true; 118 | int delay = 0; 119 | char buf; 120 | while (read(STDIN_FILENO, &buf, 1) == -1 && errno == EAGAIN) { 121 | // Sleep so we poll the screen regularly. 122 | sleepMS(1000 / FPS); 123 | if (!firstFrame) 124 | delay = std::min(maxDelayMS, delay + 1000 / FPS); 125 | 126 | // Check for CAPS LOCK status. If the key is pressed, "freeze" the 127 | // recording. 128 | const bool frozen = screencast::capsLockEnabled(); 129 | if (previousFrozen && frozen) 130 | continue; 131 | 132 | if (!previousFrozen && frozen) { 133 | // Newly frozen. Save mouse position; 134 | printf("FROZEN\n"); 135 | fprintf(fp, "//FROZEN\n"); 136 | frozenMousePosition = QCursor::pos(); 137 | previousFrozen = frozen; 138 | continue; 139 | } 140 | 141 | if (previousFrozen && !frozen) { 142 | // Newly unfrozen. Warp to the frozen mouse position. Pause for 143 | // another frame to give programs a chance to update the mouse 144 | // cursor image (and hover, etc). 145 | printf("UNFROZEN\n"); 146 | fprintf(fp, "//UNFROZEN\n"); 147 | QCursor::setPos(frozenMousePosition); 148 | previousFrozen = frozen; 149 | // XXX: I don't know why, but constructing this Cursor is necessary 150 | // to force the setPos call above to take effect *before* pausing. 151 | screencast::Cursor flushYetAnotherQtCache(config); 152 | continue; 153 | } 154 | 155 | screencast::Cursor cursor(config); 156 | QDesktopWidget *desktop = QApplication::desktop(); 157 | QPixmap screenshotPixmap = QPixmap::grabWindow( 158 | desktop->winId(), 159 | config.captureX, config.captureY, 160 | config.captureWidth, config.captureHeight); 161 | QImage screenshot = screenshotPixmap.toImage(); 162 | 163 | if (screenshot != previousImage) { 164 | QString sampleName = QString("sample_%0.png").arg(QDateTime::currentMSecsSinceEpoch()); 165 | previousImage = screenshot; 166 | screenshot.save(sampleName); 167 | fprintf(fp, "[%d,\"screen\",\"%s\"],\n", delay, sampleName.toStdString().c_str()); 168 | delay = 0; 169 | firstFrame = false; 170 | } 171 | 172 | if (cursor.imageID() != previousCursor.imageID()) { 173 | fprintf(fp, "[%d,\"cursor\",\"cursor_%u.png\",%d,%d],\n", 174 | delay, cursor.imageID(), 175 | cursor.position().x(), cursor.position().y()); 176 | delay = 0; 177 | } else if (cursor.position() != previousCursor.position()) { 178 | fprintf(fp, "[%d,\"cpos\",%d,%d],\n", 179 | delay, 180 | cursor.position().x(), cursor.position().y()); 181 | delay = 0; 182 | } 183 | previousCursor = cursor; 184 | } 185 | 186 | fprintf(fp, "]};\n"); 187 | fclose(fp); 188 | 189 | return 0; 190 | } 191 | -------------------------------------------------------------------------------- /pack_animation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (c) 2012, Sublime HQ Pty Ltd 3 | # All rights reserved. 4 | 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | # * Redistributions of source code must retain the above copyright 8 | # notice, this list of conditions and the following disclaimer. 9 | # * Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # * Neither the name of the nor the 13 | # names of its contributors may be used to endorse or promote products 14 | # derived from this software without specific prior written permission. 15 | 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 20 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | import scipy.ndimage.measurements as me 28 | import json 29 | import scipy.misc as misc 30 | import re 31 | import sys 32 | import os 33 | import cv2 34 | from cStringIO import StringIO 35 | from numpy import * 36 | from time import time 37 | 38 | # How many pixels can be wasted in the name of combining neighbouring changed 39 | # regions. 40 | SIMPLIFICATION_TOLERANCE = 512 41 | 42 | MAX_PACKED_HEIGHT = 10000 43 | 44 | def slice_size(a, b): 45 | return (a.stop - a.start) * (b.stop - b.start) 46 | 47 | def combine_slices(a, b, c, d): 48 | return (slice(min(a.start, c.start), max(a.stop, c.stop)), 49 | slice(min(b.start, d.start), max(b.stop, d.stop))) 50 | 51 | def slices_intersect(a, b, c, d): 52 | if (a.start >= c.stop): return False 53 | if (c.start >= a.stop): return False 54 | if (b.start >= d.stop): return False 55 | if (d.start >= b.stop): return False 56 | return True 57 | 58 | # Combine a large set of rectangles into a smaller set of rectangles, 59 | # minimising the number of additional pixels included in the smaller set of 60 | # rectangles 61 | def simplify(boxes, tol = 0): 62 | out = [] 63 | for a,b in boxes: 64 | sz1 = slice_size(a, b) 65 | did_combine = False 66 | for i in xrange(len(out)): 67 | c,d = out[i] 68 | cu, cv = combine_slices(a, b, c, d) 69 | sz2 = slice_size(c, d) 70 | if slices_intersect(a, b, c, d) or (slice_size(cu, cv) <= sz1 + sz2 + tol): 71 | out[i] = (cu, cv) 72 | did_combine = True 73 | break 74 | if not did_combine: 75 | out.append((a,b)) 76 | 77 | if tol != 0: 78 | return simplify(out, 0) 79 | else: 80 | return out 81 | 82 | def slice_tuple_size(s): 83 | a, b = s 84 | return (a.stop - a.start) * (b.stop - b.start) 85 | 86 | # Allocates space in the packed image. This does it in a slow, brute force 87 | # manner. 88 | class Allocator2D: 89 | def __init__(self, rows, cols): 90 | self.bitmap = zeros((rows, cols), dtype=uint8) 91 | self.available_space = zeros(rows, dtype=uint32) 92 | self.available_space[:] = cols 93 | self.num_used_rows = 0 94 | 95 | def allocate(self, w, h): 96 | bh, bw = shape(self.bitmap) 97 | 98 | for row in xrange(bh - h + 1): 99 | if self.available_space[row] < w: 100 | continue 101 | 102 | for col in xrange(bw - w + 1): 103 | if self.bitmap[row, col] == 0: 104 | if not self.bitmap[row:row+h,col:col+w].any(): 105 | self.bitmap[row:row+h,col:col+w] = 1 106 | self.available_space[row:row+h] -= w 107 | self.num_used_rows = max(self.num_used_rows, row + h) 108 | return row, col 109 | raise RuntimeError() 110 | 111 | def find_matching_rect(bitmap, num_used_rows, packed, src, sx, sy, w, h): 112 | template = src[sy:sy+h, sx:sx+w] 113 | bh, bw = shape(bitmap) 114 | image = packed[0:num_used_rows, 0:bw] 115 | 116 | if num_used_rows < h: 117 | return None 118 | 119 | result = cv2.matchTemplate(image,template,cv2.TM_CCOEFF_NORMED) 120 | 121 | row,col = unravel_index(result.argmax(),result.shape) 122 | if ((packed[row:row+h,col:col+w] == src[sy:sy+h,sx:sx+w]).all() 123 | and (packed[row:row+1,col:col+w,0] == src[sy:sy+1,sx:sx+w,0]).all()): 124 | return row,col 125 | else: 126 | return None 127 | 128 | # The script file is a JavaScript file consisting of a single variable 129 | # assignment: 130 | # script_var = Extended-JSON; 131 | # Extended-JSON is a JSON string, except that: 132 | # - It may have block (/**/) or line (//) comments. 133 | # - Trailing commas are permitted. 134 | # This function strips the comments and trailing commas out of the 135 | # Extended-JSON text and parses it to a Python object, which is returned. 136 | def parse_script_file(input): 137 | output = StringIO() 138 | quoted = False 139 | lineComment = False 140 | blockComment = False 141 | comma = False 142 | i = 0 143 | while i < len(input): 144 | if lineComment: 145 | if input[i] == '\n': 146 | lineComment = False 147 | i += 1 148 | elif blockComment: 149 | if input[i:i+2] == "*/": 150 | blockComment = False 151 | i += 2 152 | else: 153 | i += 1 154 | elif quoted: 155 | if input[i] == "\\" and i + 1 < len(input): 156 | output.write(input[i:i+2]) 157 | i += 2 158 | else: 159 | quoted = (input[i] != '"') 160 | output.write(input[i]) 161 | i += 1 162 | else: 163 | if input[i:i+2] == "//": 164 | lineComment = True 165 | i += 2 166 | elif input[i:i+2] == "/*": 167 | blockComment = True 168 | i += 2 169 | elif input[i].isspace(): 170 | # Strip whitespace. 171 | i += 1 172 | elif input[i] == ",": 173 | # Recognize and strip off trailing commas because the JSON 174 | # parser rejects them. 175 | comma = True 176 | i += 1 177 | elif input[i] in "]}": 178 | output.write(input[i]) 179 | i += 1 180 | comma = False 181 | else: 182 | if comma: 183 | output.write(",") 184 | comma = False 185 | quoted = (input[i] == '"') 186 | output.write(input[i]) 187 | i += 1 188 | assert not quoted and not blockComment 189 | m = re.match(r"^[A-Za-z0-9_]+=(.*);$", output.getvalue()) 190 | return json.loads(m.group(1)) 191 | 192 | def read_script(script_filename): 193 | with open(script_filename) as f: 194 | return parse_script_file(f.read()) 195 | 196 | def generate_animation(script_filename): 197 | assert script_filename.endswith(".js") 198 | anim_name = script_filename[:-3] 199 | script = read_script(script_filename) 200 | frames = [(index, item[2]) 201 | for (index, item) in enumerate(script["steps"]) 202 | if item[1] == "screen"] 203 | 204 | script_dir = os.path.dirname(script_filename) 205 | images = [misc.imread(os.path.join(script_dir, f)) for i, f in frames] 206 | 207 | zero = images[0] - images[0] 208 | pairs = zip([zero] + images[:-1], images) 209 | diffs = [sign((b - a).max(2)) for a, b in pairs] 210 | 211 | # Find different objects for each frame 212 | img_areas = [me.find_objects(me.label(d)[0]) for d in diffs] 213 | 214 | # Simplify areas 215 | img_areas = [simplify(x, SIMPLIFICATION_TOLERANCE) for x in img_areas] 216 | 217 | ih, iw, _ = shape(images[0]) 218 | 219 | # Generate a packed image 220 | allocator = Allocator2D(MAX_PACKED_HEIGHT, iw) 221 | packed = zeros((MAX_PACKED_HEIGHT, iw, 3), dtype=uint8) 222 | 223 | # Sort the rects to be packed by largest size first, to improve the packing 224 | rects_by_size = [] 225 | for i in xrange(len(images)): 226 | src_rects = img_areas[i] 227 | 228 | for j in xrange(len(src_rects)): 229 | rects_by_size.append((slice_tuple_size(src_rects[j]), i, j)) 230 | 231 | rects_by_size.sort(reverse = True) 232 | 233 | allocs = [[None] * len(src_rects) for src_rects in img_areas] 234 | 235 | print anim_name,"packing, num rects:",len(rects_by_size),"num frames:",len(images) 236 | 237 | t0 = time() 238 | 239 | for size,i,j in rects_by_size: 240 | src = images[i] 241 | src_rects = img_areas[i] 242 | 243 | a, b = src_rects[j] 244 | sx, sy = b.start, a.start 245 | w, h = b.stop - b.start, a.stop - a.start 246 | 247 | # See if the image data already exists in the packed image. This takes 248 | # a long time, but results in worthwhile space savings (20% in one 249 | # test) 250 | existing = find_matching_rect(allocator.bitmap, allocator.num_used_rows, packed, src, sx, sy, w, h) 251 | if existing: 252 | dy, dx = existing 253 | allocs[i][j] = (dy, dx) 254 | else: 255 | dy, dx = allocator.allocate(w, h) 256 | allocs[i][j] = (dy, dx) 257 | 258 | packed[dy:dy+h, dx:dx+w] = src[sy:sy+h, sx:sx+w] 259 | 260 | print anim_name,"packing finished, took:",time() - t0 261 | 262 | packed = packed[0:allocator.num_used_rows] 263 | 264 | misc.imsave(anim_name + "_packed_tmp.png", packed) 265 | os.system("pngcrush -q " + anim_name + "_packed_tmp.png " + anim_name + "_packed.png") 266 | os.system("rm " + anim_name + "_packed_tmp.png") 267 | 268 | # Generate JSON to represent the data 269 | new_steps = list(script["steps"]) 270 | for i in xrange(len(images)): 271 | script_index = frames[i][0] 272 | src_rects = img_areas[i] 273 | dst_rects = allocs[i] 274 | 275 | blitlist = [] 276 | 277 | for j in xrange(len(src_rects)): 278 | a, b = src_rects[j] 279 | sx, sy = b.start, a.start 280 | w, h = b.stop - b.start, a.stop - a.start 281 | dy, dx = dst_rects[j] 282 | 283 | blitlist.append( 284 | [int(dx), int(dy), int(w), int(h), int(sx), int(sy)]) 285 | 286 | new_steps[script_index] = [script["steps"][script_index][0], "blit", blitlist] 287 | 288 | packed_png_relpath = os.path.relpath(anim_name + "_packed.png", script_dir) 289 | new_steps = [[0, "blitimg", packed_png_relpath]] + new_steps 290 | f = open(anim_name + "_packed.js", "wb") 291 | f.write(os.path.basename(anim_name) + "_packed = {\n") 292 | f.write('"width" : %d,\n' % script["width"]) 293 | f.write('"height" : %d,\n' % script["height"]) 294 | f.write('"steps" : [\n') 295 | for item in new_steps: 296 | f.write(json.dumps(item) + ",\n") 297 | f.write("]};\n") 298 | f.close() 299 | 300 | 301 | if __name__ == '__main__': 302 | generate_animation(sys.argv[1]) 303 | -------------------------------------------------------------------------------- /player.js: -------------------------------------------------------------------------------- 1 | function Player(script, scriptDir) 2 | { 3 | var that = this; 4 | 5 | var kMaxCursorWidth = 32; 6 | var kMaxCursorHeight = 32; 7 | 8 | var g_scriptDir = null; 9 | var g_script = null; 10 | var g_index = -1; 11 | var g_divElement = null; 12 | var g_mainCanvas = null; 13 | var g_cursorCanvas = null; 14 | 15 | var g_blitImage = null; 16 | 17 | var g_imageCache = {}; 18 | var g_imageCount = 0; 19 | var g_imageLoadCount = 0; 20 | 21 | var g_loaded = false; 22 | var g_inCallback = false; 23 | var g_paused = true; 24 | var g_stepTimeoutID = null; 25 | 26 | this.onload = function() {}; 27 | this.onloop = function() {}; 28 | 29 | function assert(x) { 30 | // With IE9, window.console is undefined (unless the console is 31 | // opened), so we must avoid calling console.assert. 32 | if (!x) { 33 | // This line must have braces around it so that a breakpoint can be 34 | // placed on it. Affects at least Firefox 19. 35 | throw new Error("Assertion failed"); 36 | } 37 | } 38 | 39 | function imageLoaded() 40 | { 41 | g_imageLoadCount++; 42 | assert(g_imageLoadCount <= g_imageCount); 43 | if (g_imageLoadCount == g_imageCount) { 44 | // Use an extra delay in the hope that it will work around a 45 | // problem with IE9. With that browser, there is a player bug 46 | // where the first frame is not drawn, and the canvas instead 47 | // starts off transparent. 48 | window.setTimeout(finishSetup, 0); 49 | } 50 | } 51 | 52 | function finishSetup() 53 | { 54 | // Avoid flicker by executing the initial 0-delay steps before firing 55 | // the onload handler. The canvas will be painted when the handler 56 | // fires. 57 | assert(!g_loaded); 58 | for (g_index = 0; 1; g_index++) { 59 | assert(g_index < g_script.length); 60 | var delayMS = g_script[g_index][0]; 61 | if (delayMS > 0) 62 | break; 63 | executeStep(g_index); 64 | } 65 | g_loaded = true; 66 | 67 | g_inCallback = true; 68 | that.onload(); 69 | g_inCallback = false; 70 | 71 | if (!g_paused) 72 | beginCurrentStep(); 73 | } 74 | 75 | function executeStep(index) 76 | { 77 | var stepKind = g_script[index][1]; 78 | if (stepKind == "blitimg") { 79 | var url = g_scriptDir + "/" + g_script[index][2]; 80 | g_blitImage = g_imageCache[url]; 81 | } else if (stepKind == "blit") { 82 | var ctx = g_mainCanvas.getContext("2d"); 83 | var blits = g_script[index][2] 84 | for (var i = 0; i < blits.length; ++i) { 85 | var blit = blits[i]; 86 | var sx = blit[0]; 87 | var sy = blit[1]; 88 | var w = blit[2]; 89 | var h = blit[3]; 90 | var dx = blit[4]; 91 | var dy = blit[5]; 92 | ctx.drawImage(g_blitImage, sx, sy, w, h, dx, dy, w, h); 93 | } 94 | } else if (stepKind == "screen") { 95 | var url = g_scriptDir + "/" + g_script[index][2]; 96 | var ctx = g_mainCanvas.getContext("2d"); 97 | ctx.clearRect(0, 0, g_mainCanvas.width, g_mainCanvas.height); 98 | ctx.drawImage(g_imageCache[url], 0, 0); 99 | } else if (stepKind == "cpos") { 100 | g_cursorCanvas.style.left = g_script[index][2] + "px"; 101 | g_cursorCanvas.style.top = g_script[index][3] + "px"; 102 | } else if (stepKind == "cimg") { 103 | // "cimg" is an obsolete script command kept for backwards 104 | // compatibility with existing animations. 105 | var url = g_scriptDir + "/" + g_script[index][2]; 106 | var ctx = g_cursorCanvas.getContext("2d"); 107 | ctx.clearRect(0, 0, g_cursorCanvas.width, g_cursorCanvas.height); 108 | ctx.drawImage(g_imageCache[url], 0, 0); 109 | } else if (stepKind == "cursor") { 110 | // Clear the canvas first, then move it, then draw the new cursor. 111 | var url = g_scriptDir + "/" + g_script[index][2]; 112 | var ctx = g_cursorCanvas.getContext("2d"); 113 | ctx.clearRect(0, 0, g_cursorCanvas.width, g_cursorCanvas.height); 114 | g_cursorCanvas.style.left = g_script[index][3] + "px"; 115 | g_cursorCanvas.style.top = g_script[index][4] + "px"; 116 | ctx.drawImage(g_imageCache[url], 0, 0); 117 | } else if (stepKind == "nop") { 118 | // Do nothing. This step kind only exists for convenience in adding 119 | // delays to a script. 120 | } else { 121 | alert("Invalid step in animation script: " + g_script[index]); 122 | } 123 | } 124 | 125 | function beginCurrentStep() 126 | { 127 | assert(g_loaded && !g_paused); 128 | assert(g_stepTimeoutID === null); 129 | var delayMS = g_script[g_index][0]; 130 | g_stepTimeoutID = window.setTimeout(function() { 131 | g_stepTimeoutID = null; 132 | executeStep(g_index); 133 | g_index++; 134 | if (g_index == g_script.length) { 135 | g_index = 0; 136 | g_inCallback = true; 137 | that.onloop(); 138 | g_inCallback = false; 139 | } 140 | if (!g_paused) 141 | beginCurrentStep(); 142 | }, delayMS); 143 | } 144 | 145 | this.start = function() { 146 | g_paused = false; 147 | if (g_loaded && !g_inCallback && g_stepTimeoutID === null) { 148 | beginCurrentStep(); 149 | } 150 | } 151 | 152 | this.pause = function() { 153 | g_paused = true; 154 | if (g_loaded && !g_inCallback && g_stepTimeoutID !== null) { 155 | window.clearTimeout(g_stepTimeoutID); 156 | g_stepTimeoutID = null; 157 | } 158 | } 159 | 160 | this.isPaused = function() { 161 | return g_paused; 162 | } 163 | 164 | // Note that this function returns false for the duration of the onload 165 | // callback. 166 | this.isLoaded = function() { 167 | return g_loaded; 168 | } 169 | 170 | var widthPx = script["width"]; 171 | var heightPx = script["height"]; 172 | g_divElement = document.createElement("div"); 173 | g_divElement.style.position = "relative"; 174 | g_divElement.style.width = widthPx + "px"; 175 | g_divElement.style.height = heightPx + "px"; 176 | g_divElement.style.overflow = "hidden"; 177 | this.element = g_divElement; 178 | g_mainCanvas = document.createElement("canvas"); 179 | g_mainCanvas.style.position = "absolute"; 180 | g_mainCanvas.style.left = "0px"; 181 | g_mainCanvas.style.top = "0px"; 182 | g_mainCanvas.width = widthPx; 183 | g_mainCanvas.height = heightPx; 184 | g_divElement.appendChild(g_mainCanvas); 185 | g_cursorCanvas = document.createElement("canvas"); 186 | g_cursorCanvas.style.position = "absolute"; 187 | g_cursorCanvas.width = kMaxCursorWidth; 188 | g_cursorCanvas.height = kMaxCursorHeight; 189 | g_divElement.appendChild(g_cursorCanvas); 190 | 191 | g_scriptDir = scriptDir 192 | g_script = script["steps"] 193 | for (var i = 0; i < g_script.length; ++i) { 194 | var stepKind = g_script[i][1]; 195 | if (stepKind == "blitimg" || 196 | stepKind == "screen" || 197 | stepKind == "cimg" || 198 | stepKind == "cursor") { 199 | g_imageCount++; 200 | var url = g_scriptDir + "/" + g_script[i][2]; 201 | var image = new Image(); 202 | g_imageCache[url] = image; 203 | image.onload = imageLoaded; 204 | image.src = url; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /screencast.pro: -------------------------------------------------------------------------------- 1 | QT += core gui 2 | 3 | TARGET = screencast 4 | CONFIG += console 5 | CONFIG -= app_bundle 6 | 7 | TEMPLATE = app 8 | 9 | SOURCES += \ 10 | CaptureConfig.cpp \ 11 | CursorCommon.cpp \ 12 | MurmurHash3.cpp \ 13 | main.cpp 14 | 15 | macx { 16 | LIBS += -framework AppKit 17 | 18 | SOURCES += \ 19 | CapsLockStateOSX.cpp \ 20 | CursorOSX.cpp 21 | 22 | OBJECTIVE_SOURCES += \ 23 | ObjectiveCBridge.m 24 | HEADERS += \ 25 | ObjectiveCBridge.h 26 | 27 | } else { 28 | SOURCES += \ 29 | CapsLockStateX11.cpp \ 30 | CursorX11.cpp 31 | 32 | CONFIG += link_pkgconfig 33 | PKGCONFIG += x11 xfixes 34 | } 35 | 36 | HEADERS += \ 37 | CaptureConfig.h \ 38 | CapsLockState.h \ 39 | Cursor.h \ 40 | MurmurHash3.h 41 | 42 | # HACK: Avoid stripping the scripts. 43 | QMAKE_STRIP = 44 | 45 | target.path = / 46 | scripts.path = / 47 | scripts.files += \ 48 | player.js \ 49 | pack_animation.py 50 | 51 | INSTALLS += \ 52 | scripts \ 53 | target 54 | --------------------------------------------------------------------------------